PageSourceSearch

https://codelamp.co.uk/js/solar-display.js?build=BUILDTOKEN

js codelamp.co.uk collected 2026-10-04 13:34:39 UTC 21,555 bytes, 841 lines download raw bytes

1/**
2 * Creates a sun diagram to display a number of items in a fun way
3 */
4var solarDisplay = {
5
6  /**
7   *
8   */
9  defaults: {
10    width: 600,
11    height: 600,
12    innerRadius: 100,
13    barMaxSize : 200,
14    items: [
15      { title: 'define', frequency: 1 },
16      { title: 'some', frequency: 0.7 },
17      { title: 'items', frequency: 0.5 },
18      { title: 'first', frequency: 0.3 }
19    ]
20  },
21
22  vars: {
23    built: false,
24    focusedBar: false,
25    barPosition: {},
26    barTransitioning: false
27  },
28
29  containerOffset: {
30    x: 0,
31    y: 0
32  },
33
34  /**
35   * @static
36   */
37  transformCentralText: function(d){
38    var b;
39    try { b = this.getBBox(); } catch ( ex ) { b = { width: 0, height: 0 }; };
40    return 'translate(' + (-b.width/2) + ',' + (b.height/4) + ')';
41  },
42
43  /**
44   *
45   */
46  transformBarElementsFunction: function(){
47    var opt = this.options;
48    return function(d, index) {
49      return 'translate('
50          //+ (opt.dims.width / 2 + opt.innerRadius * Math.cos(opt.theta * index)) + ','
51          //+ (opt.dims.height / 2.2 - opt.innerRadius * Math.sin(opt.theta * index)) +
52          + (opt.innerRadius * Math.cos(opt.theta * index)) + ','
53          + (- opt.innerRadius * Math.sin(opt.theta * index)) +
54        ') ' +
55        'rotate(' + (98 - index * opt.thetaD) + ')';
56    };
57  },
58
59  /**
60   *
61   */
62  transformLabelElementsFunction: function(){
63    var opt = this.options;
64    return function(d, index) {
65      return 'rotate(90) translate(' + ((opt.innerRadius * 2) + 10) + ', ' + ((-opt.barWidth * 0.15) - 2) + ')';
66    };
67  },
68
69  /**
70   *
71   */
72  transformLabelTextFunction: function(){
73    var opt = this.options;
74    return function(d, index) {
75      var b;
76      try { b = this.getBBox(); } catch ( ex ) { b = { width: 0, height: 0 }; };
77      return index < opt.dataSize/4 || index > (opt.dataSize - opt.dataSize/4)
78        ? 'scale(-1,-1) translate(' + (-b.width) + ',' + (b.height-8) + ')'
79        : ''
80      ;
81    };
82  },
83
84  /**
85   * Create an instance of solarDisplay
86   */
87  translate: function(x, y) {
88    return "translate(" + x + "," + y + ") ";
89  },
90
91  scale: function(s){
92    return s < 1 ? "scale(" + s + ") " : " ";
93  },
94
95  /**
96   * Create an instance of solarDisplay
97   */
98  create: function(options) {
99    return this.prep.call(Object.create(this), options||{});
100  },
101
102  /**
103   * prep an instance of solarDisplay with unique references
104   */
105  prep: function(options){
106
107    var sol = this,
108        opt = this.options = {}
109    ;
110
111    opt.title = options.title || "";
112    opt.x = options.x || 0;
113    opt.y = options.y || 0;
114    opt.dims = {
115      width: options.width || this.defaults.width,
116      height: options.height || this.defaults.height
117    };
118    opt.container = options.container
119      ? d3.select(options.container)
120      : d3.select('body')
121    ;
122    this.state = 'unstarted';
123    this.ui = {};
124    this.ui.solarContainer = opt.container;
125    this.svg = this.ui.solarContainer.append('svg').attr(opt.dims).classed('solar-display', true)
126      .style({
127        width: '100%',
128        height: '100%'
129      })
130    ;
131    this.data = options.items || this.defaults.items;
132
133    opt.dataSize = this.data.length;
134    opt.innerRadius = options.innerRadius || this.defaults.innerRadius;
135    opt.barWidth = Math.max(Math.min(2 * Math.PI * opt.innerRadius / opt.dataSize, 80), 3);
136    opt.theta = 2 * Math.PI / opt.dataSize;
137    opt.thetaD = 360 / opt.dataSize;
138    opt.barMaxSize = options.barMaxSize || this.defaults.barMaxSize;
139    opt.outerRadius = opt.innerRadius + opt.barMaxSize;
140
141    //this.svg.attr('preserveAspectRatio', 'xMidYMid');
142    //this.svg.attr('viewBox', ([0, 0, opt.dims.width, opt.dims.height]).join(' '))
143
144    this.ui.container = this.svg.append('g');
145
146    if ( options.hidden ) {
147      this.hide();
148    }
149    else {
150      this.show();
151    }
152
153    // click off
154    d3.select('body').on('click', function(e){
155      sol.clickOff();
156    });
157
158    return this;
159
160  },
161
162  show: function(){
163    this.ui.solarContainer.style('display', 'block');
164    this.vars.hidden = false;
165    (!this.vars.built) && this.buildAttempt();
166  },
167
168  hide: function(){
169    this.ui.solarContainer.style('display', 'none');
170    this.vars.hidden = true;
171  },
172
173  toggle: function(){
174    if ( this.vars.hidden ) {
175      this.show();
176    }
177    else {
178      this.hide();
179    }
180    return this;
181  },
182
183  /**
184   * Make sure we can getBBox before building.
185   * Otherwise dimensions will be alll screwy.
186   */
187  buildAttempt: function(){
188    var sol = this, build = function(){
189      sol.ui.container[0][0].getBBox();
190      sol.build();
191      sol.animateIn(function(){
192        //sol.animateExplode();
193      });
194    };
195    try { build(); } catch ( ex ) {
196      var iid = setInterval(function(){
197        try { build(); clearInterval(iid); } catch ( ex ) { };
198      }, 500);
199    }
200  },
201
202  build: function(){
203
204    var sol = this,
205        opt = this.options
206    ;
207
208    this.vars.built = true;
209
210    /// TODO: this could be improved to handle wide but short displays
211    d3.select(window).on('resize', function(){ sol.resize(); });
212
213    /// clear existing bars
214    this.ui.container.selectAll(".bar").remove();
215
216    this.ui.txtcontainer = this.ui.container.append('g').style('opacity', 0);
217    this.ui.txtline1 = this.ui.txtcontainer.append('text')
218      .attr('fill', 'white')
219      .style('text-anchor', 'middle')
220      .html('Press a light bar to view, darker bars yet to have content:')
221    ;
222    this.ui.txtarrow1 = this.ui.txtcontainer.append('text').html('\u2935').attr({
223      x: 190,
224      y: 12,
225      fill: 'white',
226      'font-size': '1.5em'
227    });
228
229    this.resize();
230
231    /// create the central group
232    this.ui.centralDisc = this.ui.container.append("g")
233      //.attr("transform", this.translate(opt.dims.width / 2, opt.dims.height / 2.2))
234      .attr("transform", this.translate(0, 0))
235      .on("mouseover", function(){
236        sol.animateExplode();
237      })
238      //.on("mouseout", function(){
239      //  sol.animateImplode();
240      //})
241    ;
242
243    /// create new bars
244    this.ui.bars = this.ui.container.selectAll(".bar")
245      .data(this.data)
246        .enter()
247          .append("g")
248            .attr("class", "bar")
249            .attr("transform", this.transformBarElementsFunction())
250            .attr('x', 0)
251            .attr('y', opt.innerRadius * 2)
252            .on("click", function( d ){
253              d3.event.stopPropagation();
254              sol.focusBar( this, d );
255            })
256    ;
257
258    /// create the rectangles within in the bar groups
259    this.ui.rects = this.ui.bars.append("rect")
260      .attr("class", function( d ){
261        return d && d.description
262          ? 'bar-itself with-description'
263          : 'bar-itself'
264        ;
265      })
266      .attr("width", opt.barWidth-2)
267      .attr('x', 0)
268      .attr('y', opt.innerRadius*2)
269      //.attr('fill', 'white')
270      .attr("height", 0)
271      .style("opacity", 0)
272    ;
273
274    /// create the labels
275    this.ui.labels = this.ui.bars
276      .append('g')
277        .style("opacity", 0)
278        .attr("transform", this.transformLabelElementsFunction())
279    ;
280
281    /// create the label text
282    this.ui.labelText = this.ui.labels
283      .append('text')
284        .text(function(d, index){return d.title;})
285        .style('fill-opacity', 0.5)
286        .attr({
287          "class": "text",
288          "width": "100%",
289          "height": "100%",
290          "fill": "black",
291          "pointer-events": "none",
292          "font-size": Math.max(Math.min(opt.barWidth * 0.8, 18), 12),
293          "transform": this.transformLabelTextFunction()
294        })
295    ;
296
297    /// create the central circle
298    this.ui.centralDisc.append("circle")
299      .attr({
300        "cx": 0,
301        "cy": 0,
302        "r": opt.innerRadius * 2
303      })
304      .attr("fill", "rgba(0,0,0,0)")
305    ;
306
307    /// create the central text
308    this.ui.centralText = this.ui.centralDisc.append("text")
309      .text(opt.title)
310      .attr({
311        "x": 0,
312        "y": 0,
313        "fill": "white",
314        "font-size": '1.6em',
315        "line-height": '1.6em'
316      })
317      .attr({
318        "transform": solarDisplay.transformCentralText
319      })
320      .classed("main-title", true)
321      .style({
322        'opacity': 0,
323        'cursor': 'default'
324      })
325    ;
326
327    var con = this.ui.solarContainer.node();
328    this.ui.slideBars = this.ui.container.selectAll('.sbars')
329      .data([0,0.2,0.5,0.3,0.7,0.1,0.9,1])
330        .enter()
331          .append('rect')
332            .attr('rx', 6)
333            .attr('ry', 6)
334            .attr('height', opt.dims.height * 2)
335            .attr('width', Math.round(opt.dims.width / 8))
336            .attr('y', con.offsetHeight)
337            .attr('x', function(d, i){
338              return -opt.dims.width/2 + i * (opt.dims.width / 8);
339            })
340            .attr('fill', 'black')
341            .on("click", function( d ){
342              d3.event.stopPropagation();
343            })
344    ;
345
346    this.resize();
347
348  },
349
350  clickOff: function(){
351    if ( this.vars.focusedBar ) {
352      this.focusClose();
353    }
354  },
355
356  animateSlideBarsIn: function(callback){
357    var sol = this;
358    var opt = this.options;
359    var con = this.ui.solarContainer.node();
360    this.ui.slideBars
361      .transition()
362        .delay(function(d){
363          return d * 500;
364        })
365        .attr('rx', 0)
366        .attr('ry', 0)
367        .attr('y', -con.offsetHeight / 2)
368        .duration(500)
369        .call(this.afterAllEnded, function(){
370          callback && callback.apply && callback.apply(sol);
371        })
372    ;
373  },
374
375  animateSlideBarsOut: function(delay){
376    var opt = this.options;
377    var con = this.ui.solarContainer.node();
378    this.ui.slideBars
379      .transition()
380        .delay(function(d){
381          return (delay||0) + d * 500;
382        })
383        .attr('rx', 6)
384        .attr('ry', 6)
385        .attr('y', con.offsetHeight)
386        .duration(500)
387    ;
388  },
389
390  /**
391   *
392   */
393  focusBar: function ( clicked, d ){
394    var sol = this;
395    if ( this.vars.focusedBar ) {
396      this.focusClose();
397    }
398    else {
399      if ( d.description ) {
400        this.animateSlideBarsIn(function(){
401          sol.focusContent(d);
402        });
403        /// configure variables
404        this.vars.focusedBar = clicked;
405        this.ui.focusedBar = d3.select(clicked);
406        this.vars.previousBarTransform = this.ui.focusedBar.attr('transform');
407        /*
408        /// trigger transitions
409        this.focusCentralDiscTransitionOut();
410        this.vars.barTransitioning = true;
411        this.focusBarTransitionIn(function(){
412          this.vars.barTransitioning = false;
413          this.resize();
414        });
415        /// display the content for this bar
416        this.focusContent(d);
417        */
418      }
419      /// trigger any configured functions on click
420      this.options.click && this.options.click(d);
421    }
422  },
423
424  /**
425   *
426   */
427  focusContent: function ( d ){
428    var sol = this,
429        con = this.ui.solarContainer.node(),
430        w, h,
431        fs = 'inherit',
432        lh = 'inherit',
433        ics = 28/32
434    ;
435    if ( con.offsetWidth < this.options.dims.width ) {
436      w = con.offsetWidth / 2;
437      h = this.options.dims.height / 2 - 100;
438      fs = '1.6em';
439      lh = '1.3em';
440      ics = 28/20;
441    }
442    else {
443      w = this.options.dims.width / 2 - 100,
444      h = this.options.dims.height / 2 - 100;
445    }
446    this.ui.htmlWrapper = this.ui.container.append('foreignObject')
447      .attr({
448        x: -w,
449        y: -h * 1.5,
450        width: w * 2,
451        height: h * 3
452      })
453      .style('opacity', 0)
454      .on("click", function( d ){
455        d3.event.stopPropagation();
456      })
457    ;
458    this.ui.htmlContent = this.ui.htmlWrapper
459      .append('xhtml:div')
460      .attr('xmlns', 'http://www.w3.org/1999/xhtml')
461      .style({
462        'color': 'white',
463        'text-align': 'left',
464        'display': 'block',
465        'font-size': fs,
466        'line-height': lh
467      })
468      .html('<h3>' + d.title + '</h3>' + d.description)
469    ;
470    this.ui.closeContent = sol.ui.container.append('g')
471      .attr('transform', this.translate(w, - con.offsetHeight / 2 + 10))
472      .style('cursor', 'pointer')
473      .on('click', function(){ sol.focusClose(); })
474      .on('mouseover', function(){ sol.ui.closeIcon.transition().attr('fill', 'orange').duration(300); })
475      .on('mouseout', function(){ sol.ui.closeIcon.transition().attr('fill', 'white').duration(300); });
476    ;
477    this.ui.closeIcon = this.ui.closeContent.append('path')
478      .attr('d', 'M0,7L7,0L16,10L26,0L32,7L23,16L32,25L26,31L16,22L7,31L0,24L9,16Z')
479      .attr('fill', 'white')
480      .attr('transform', 'scale(' + ics + ')')
481    ;
482    this.ui.fadeRect = this.ui.container.append('rect')
483      .attr({
484        x: -w * 1.25,
485        y: -h * 1.5,
486        width: w * 2.5,
487        height: h * 4,
488        fill: '#000',
489        opacity: 1
490      })
491      .on("click", function( d ){
492        d3.event.stopPropagation();
493      })
494    ;
495    this.resize();
496    // this works in firefox (but not in chrome!)
497    this.ui.htmlWrapper
498      .transition()
499        .style('opacity', 1)
500        .duration(500)
501    ;
502    // this works in chrome (but not in firefox!)
503    this.ui.fadeRect && this.ui.fadeRect.transition()
504      .attr('opacity', 0)
505      .duration(500)
506  },
507
508  focusClose: function(){
509    var sol = this;
510    this.animateSlideBarsOut(300);
511    this.ui.closeContent.transition()
512      .attr('opacity', 1)
513      .duration(500)
514      .remove()
515    ;
516    this.ui.fadeRect && this.ui.fadeRect.transition()
517      .attr('opacity', 1)
518      .duration(500)
519      .call(this.afterAllEnded, function(){
520        sol.ui.htmlWrapper.remove();
521      })
522      .remove()
523    ;
524    /*
525    this.focusCentralDiscTransitionIn();
526    this.focusBarTransitionOut();
527    this.ui.htmlContent.transition()
528      .delay(0)
529      .style('opacity', 0)
530      .duration(500)
531    ;
532    */
533    this.vars.focusedBar = null;
534  },
535
536  /**
537   *
538   */
539  focusBarTransitionIn: function( callback ){
540    var sol = this, opt = this.options;
541    this.ui.txtcontainer
542      .transition()
543        .delay(2000)
544        .style('opacity', 0)
545        .duration(300)
546    ;
547    this.ui.bars.transition()
548      .delay(0)
549      .style('opacity', function(){
550        return this === sol.vars.focusedBar ? 1 : 0;
551      })
552      .attr('transform', function(d, index){
553        var item = d3.select(this), rect = item.select('rect'), x, y, w, h, r, p = 15;
554        if ( this === sol.vars.focusedBar ) {
555          if ( index < opt.dataSize/4 || index > (opt.dataSize - opt.dataSize/4) ) {
556            y = parseInt(rect.attr('y'), 10);
557            h = parseInt(rect.attr('height'), 10);
558            x = y + h + p - sol.containerOffset.x + 35;
559            y = p;// - sol.containerOffset.y/2;
560            r = 90;
561          }
562          else {
563            y = parseInt(rect.attr('y'), 10);
564            w = parseInt(rect.attr('width'), 10);
565            x = - (y - p + sol.containerOffset.x) + 35;
566            y = w + p;// - sol.containerOffset.y/2;
567            r = -90;
568          }
569          sol.vars.barPosition = {
570            x: x + ( sol.containerOffset ? sol.containerOffset.x : 0),
571            y: y,
572            r: r
573          };
574          return 'translate(' + x + ',' + y + ') rotate(' + r + ')';
575        }
576        else {
577          return item.attr('transform');
578        }
579      })
580      .ease('cubic-in-out')
581      .duration(1000)
582      .call(this.afterAllEnded, function(){
583        callback && callback.apply && callback.apply(sol);
584      })
585    ;
586  },
587
588  /**
589   *
590   */
591  focusBarTransitionOut: function( callback ){
592    var sol = this;
593    this.ui.bars.transition()
594      .delay(0)
595      .style('opacity', 1)
596      .attr('transform', function(){
597        return (this === sol.vars.focusedBar)
598          ? sol.vars.previousBarTransform
599          : d3.select(this).attr('transform')
600        ;
601      })
602      .ease('cubic-in-out')
603      .duration(1000)
604      .call(this.afterAllEnded, function(){
605        callback && callback.apply && callback.apply(sol);
606      })
607    ;
608  },
609
610  /**
611   *
612   */
613  focusCentralDiscTransitionIn: function(){
614    this.ui.centralDisc.transition()
615      .delay(0)
616      .style('opacity', 1)
617      .ease('cubic-in-out')
618      .duration(500)
619    ;
620  },
621
622  /**
623   *
624   */
625  focusCentralDiscTransitionOut: function(){
626    this.ui.centralDisc.transition()
627      .delay(0)
628      .style('opacity', 0)
629      .ease('cubic-in-out')
630      .duration(500)
631    ;
632  },
633
634  /**
635   *
636   */
637  afterAllEnded: function( transition, callback ){
638     var n = 0;
639     transition.each(function(){++n;});
640     transition.each("end", function(){ if(!--n) callback.apply(this, arguments); });
641   },
642
643  /**
644   *
645   */
646  animateExplode: function(){
647    if ( !this.vars.built ) {
648      return;
649    }
650    this.ui.solarContainer.classed('blur-back', true);
651    if ( this.state != 'exploding' && this.state != 'started'  && this.state != 'looping' && this.state != 'unstarted' ) {
652      var sol = this, opt = this.options;
653      this.state = 'exploding';
654      this.ui.txtcontainer
655        .transition()
656          .delay(2000)
657          .style('opacity', 1)
658          .duration(300)
659      ;
660      this.ui.rects
661        .transition()
662          .delay(function(d, i){return i * 30;})
663          .attr('height', function(d, i){ return d.frequency * opt.barMaxSize; })
664          .ease('elastic')
665          .duration(1500)
666          .call(this.afterAllEnded, function(){
667            sol && (sol.state = 'exploded');
668          })
669      ;
670      this.ui.labels
671        .transition()
672          .delay(function(d,i,k){return i * 30;})
673          .style('opacity', 1)
674          .duration(500)
675      ;
676    }
677  },
678
679  /**
680   *
681   */
682  animateImplode: function(){
683    if ( !this.vars.built ) {
684      return;
685    }
686    if ( this.state != 'imploding' ) {
687      var sol = this, opt = this.options;
688      this.state = 'imploding';
689      sol.animateLoop(100, function(){
690        this.state = 'ready';
691      });
692      this.ui.labels
693        .transition()
694          .style('opacity', 0)
695          .duration(500)
696      ;
697    }
698  },
699
700  /**
701   * Trigger the waiting loop
702   */
703  animateLoop: function(time, callback){
704    if ( !time ) { time = 500; }
705    var opt = this.options;
706    var sol = this;
707    var loop = function(){
708      if ( sol.state != 'looping' ) return;
709      d3.select(this)
710        .transition()
711        .attr('height', 5)
712        .ease('quad')
713        .duration(500)
714        .each("end", function(){
715          if ( sol.state != 'looping' ) return;
716          d3.select(this)
717            .transition()
718            .attr('height', 10)
719            .ease('quad')
720            .duration(500)
721            .each("end", function(){
722              loop.call(this);
723            });
724        });
725    };
726    this.state = 'looping';
727    this.ui.rects
728      .transition()
729        .delay(function(d,i,k){ return i * ((time + 500) / opt.dataSize); })
730        .attr('height', 10)
731        .style('opacity', 1)
732        .ease('quad')
733        .duration(time)
734        .call(this.afterAllEnded, function(){
735          callback && callback.apply(sol);
736        })
737        //.each("end", loop);
738    ;
739  },
740
741  /**
742   * Initial animation
743   */
744  animateIn: function(callback){
745    if ( !this.vars.built ) {
746      return;
747    }
748    var sol = this;
749    this.resize();
750    this.state = 'started';
751    this.ui.rects.attr('index', function(d,i,k){ return k; });
752    this.ui.centralText
753      .transition()
754        .style('opacity', 1)
755        .duration(2000)
756        .each("end", function(){
757          sol.state = 'ready';
758          callback && callback.apply(sol);
759        })
760    ;
761    this.animateLoop(500);
762  },
763
764  getContentMargin: function(){
765    if ( this.containerOffset && this.containerOffset.width > 800 ) {
766      return {
767        x: 100,
768        y: 50
769      };
770    }
771    else if ( this.containerOffset && this.containerOffset.width > 600 ) {
772      return {
773        x: 50,
774        y: 50
775      };
776    }
777    else {
778      return {
779        x: 30,
780        y: 50
781      };
782    }
783  },
784
785  resize: function(){
786    if ( !this.vars.built ) { return; }
787    var opt = this.options, con = this.ui.solarContainer.node();
788    var co = this.containerOffset;
789    var tb = this.ui.txtcontainer.node().getBBox();
790    var cb = this.ui.container.node().getBBox();
791    var cs = 1;
792
793    co.width = con.offsetWidth;
794    co.height = con.offsetHeight;
795    co.x = co.width / 2;
796    co.y = co.height / 2;
797
798    if ( co.width < cb.width ) {
799      cs = co.width / cb.width * 1.4;
800    }
801
802    this.ui.container.attr('transform', this.translate(co.x, co.y) + this.scale(cs));
803    this.ui.txtcontainer.attr('transform', this.translate(0, -co.y + tb.height/2 + 10));
804
805    /*
806    if ( this.ui.htmlWrapper ) {
807      var w = opt.dims.width / 2 - 100;
808      var h = opt.dims.height / 2 - 100;
809      this.ui.htmlWrapper.attr({
810        this.ui.htmlWrapper = this.ui.container.append('foreignObject')
811          .attr({
812            x: -w,
813            y: -h * 1.5,
814            width: w * 2,
815            height: h * 3
816          })
817    }
818    */
819
820    /*
821    if ( this.vars.focusedBar ) {
822      var bp = this.vars.barPosition,
823          cm = this.getContentMargin()
824      ;
825      if ( !this.vars.barTransitioning ) {
826        this.ui.focusedBar.attr('transform', 'translate(' + (bp.x - tx) + ',' + bp.y + ') rotate(' + bp.r + ')');
827      }
828      this.ui.closeContent.attr('transform', 'translate(' + (this.containerOffset.width - 86 - tx) + ',20)');
829      this.ui.htmlWrapper
830        .attr({
831          x: cm.x - tx,
832          y: cm.y + 20,
833          width: this.containerOffset.width - cm.x * 2,
834          height: this.containerOffset.height - cm.y * 2
835        })
836      ;
837    }
838    */
839  }
840
841};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.