Milan Marketplace

Project Summary

Milan Marketplace needed a designer and front-end developer to design their mobile apps and web app and be able to use their front–end framework, Twitter Bootstrap, to make adjustments to their current web app. Although I was hired mainly to design, Milan's needs changed, so a large portion of my time was spent writing JavaScript to build them custom analytics for their web app. I was eager to try something beyond my capabilities, so I accepted the challenge...and succeeded.

What I Did

Technologies Used

  • Photoshop, Illustrator
  • HTML
  • CSS, SASS
  • JavaScript, jQuery, Ajax
  • JSON
  • Git Version Control, GitHub
  • some Ruby
  • Ruby on Rails framework
  • Twitter Bootstrap framework

Custom JavaScript Analytics

Planning & User Interface Design

The purpose of building custom analytics was to show how users were using Milan's API in all iPhone and Android apps in each section of the portal (the web app). We needed to show how many views and clicks there were for each section of the portal (locations, guides, promotions, events, banners, and featured ads). We also wanted to show where users were in relation to store locations as they viewed and clicked in the mobile apps. To do this, I decided to visualize the data with line charts and maps.

Before I began, each section of the portal consisted only of tables of data that linked to more detailed information and editing capabilities. To keep the current flow of information, I opted to add the analytics to each section of the portal. Since those data tables were used as secondary navigation, I didn't want the analytics to distract from that, so the analytics are automatically hidden with the option of displaying them.

analytics chart analytics map analytics

Instead of only showing sums of views and clicks, we needed to let the user choose to see only analytics relating to a particular store location. To do that, I added radio buttons in the data tables that change the analytics data above. It seemed like the most practical, logical way to accomodate that capability. In addition to the radio buttons, the user needed to be able to control the date range of what they were being shown, so I provided a select box for that.

It didn't make visual sense to display the views and clicks combined in the maps, so I separated them and added toggle buttons to let the user choose. The CEO required a data table to summarize all of the information.

Build

To build the analytics, I utilized the gRaphael linechart plugin for the charts and the Google JavaScript API with Heatmap Layer for the Maps. All of the data is stored in local JSON files (one for charts, one for maps) to make it easy to generate the data with ruby. The JSON is loaded with Ajax, and that function is called when users either change the date range with the select box, or change the chart/map toggle buttons. The setFile() is what changes the file.

function setFile(whichFile){
	selectedDateRange = $jq('.date-range option:selected').val();
	jsonFile = "/assets/analytics-data-" +whichFile+ ".json?target_month=" + selectedDateRange;
	if (typeof(analytics_url_template) === "string") {
		jsonFile = analytics_url_template
		jsonFile = jsonFile.replace('para_target', whichFile)
		jsonFile = jsonFile.replace('para_target_month', selectedDateRange)
	}
	if(whichFile == 'charts'){
		refreshChartAnalytics();
	} else if (whichFile == 'maps'){
		refreshMapAnalytics();
	}
	return jsonFile;
}

Other various functions are called with user selections to change the JSON data shown in the analytics.

Because there are multiple sections in the portal using analytics, it was important for speed to limit the code the code that ran. I gave each section of the portal an ID so that I could make initialize functions to determine which IDs were available and to only run the necessary code – initCharts() and initMaps().

Here's analytics.js in it's entirety:


$jq(document).ready(function(){
if ($jq('.analytics').length){//if .analytics on page
  var whichAnalytics = "charts";
  var selectedDateRange,
      jsonFile;
  var currentChartID = $jq('input[name=choose-chart-totals]:checked').val();
  var currentMapID = $jq('input[name=choose-map-location]:checked').val();
  var chartRadios = $jq('.single-chart').length;

  //Nav
    $jq('.date-range').hide();
    $jq('.analytics').hide();
    $jq('th.chart-radio,td.chart-radio').hide();
    $jq('th.map-radio,td.map-radio').hide();
    $jq('.hide').removeClass('hide');//show chart-key
    $jq('.choose-map-group').hide();
    $jq('#locationMap, #guideMap, #promotionMap, #eventMap, #bannerMap, #featuredMap').hide();

    if(chartRadios == '0'){
      $jq('#showhide-analytics-btn').hide();
    }

    //Show/Hide analytics
    $jq('#showhide-analytics-btn').toggle(
      function(){
        $jq('.date-range').show();
        $jq('.analytics').slideDown();
        $jq(this).html('Hide Analytics');
        $jq('th.chart-radio,td.chart-radio').show();
        $jq('.map-btn').attr("checked", false);
        $jq('.map-btn').removeClass('active');
        $jq('.chart-btn').attr("checked", true);
        $jq('.chart-btn').addClass('active');
        $jq('#locationMap, #guideMap, #promotionMap, #eventMap, #bannerMap, #featuredMap').hide();
        $jq('.choose-map-group').hide();
        $jq('#locationChart, #guideChart, #promotionChart, #eventChart, #bannerChart, #featuredChart').show();
        $jq('.chart-key').show();
        $jq('.chart-x-label').show();
        if(chartRadios == '1'){
          $jq('th.chart-radio,td.chart-radio').hide();
          $jq('th.map-radio,td.map-radio').hide();
        }
        //determineRadioBtns();
      },
      function(){
        $jq('.date-range').hide();
        $jq('.analytics').slideUp();
        $jq(this).html('Show Analytics');
        $jq('th.chart-radio,td.chart-radio').hide();
        $jq('th.map-radio,td.map-radio').hide();
      }
    );

    //1st radio btns in main data table auto selected
      if ($jq('input[name=choose-chart-totals]').length){//if on page
        $jq('input[name=choose-chart-totals]')[0].checked = true;
      }
      if ($jq('input[name=choose-map-location]').length){//if on page
        $jq('input[name=choose-map-location]')[0].checked = true;
      }

    function setChart(){
      //radio btns
      $jq('.chart-btn').attr("checked", true);
      $jq('.map-btn').attr("checked", false);
      $jq('.map-btn').removeClass('active');
      $jq('.chart-btn').addClass('active');
      //hide other options
      $jq('.choose-map-group').hide();
      $jq('#locationMap, #guideMap, #promotionMap, #eventMap, #bannerMap, #featuredMap').hide();
      //display selected
      $jq('.chart-key').show();
      $jq('.chart-x-label').show();
      $jq('#locationChart, #guideChart, #promotionChart, #eventChart, #bannerChart, #featuredChart').show();
      refreshChartAnalytics();
      determineRadioBtns();
    }

    function sMap(){
      //btns
      $jq('.chart-btn').attr("checked", false);
      $jq('.chart-btn').removeClass('active');
      $jq('.map-btn').attr("checked", true);
      $jq('.map-btn').addClass('active');
      //hide other options
      $jq('.chart-key').hide();
      $jq('.chart-x-label').hide();
      $jq('#locationChart, #guideChart, #promotionChart, #eventChart, #bannerChart, #featuredChart').hide();
      //display selected
      $jq('.choose-map-group').show();
      $jq('#locationMap, #guideMap, #promotionMap, #eventMap, #bannerMap, #featuredMap').show();
      determineRadioBtns();
    }

    //Chart or Map
    $jq('.choose-analytics-btns').change(function(){
      if($jq('input[name=choose-analytics]:checked').val()=='chart'){
        setChart();
      } else if($jq('input[name=choose-analytics]:checked').val()=='map'){
        sMap();
      }
    });

    function determineRadioBtns(){
      if($jq('#showhide-analytics-btn').html() == 'Hide Analytics'){
        if(chartRadios > '1'){
          if($jq('input[name=choose-analytics]:checked').val()=='chart'){
            $jq('th.chart-radio,td.chart-radio').show();
            $jq('th.map-radio,td.map-radio').hide();
          } else if($jq('input[name=choose-analytics]:checked').val()=='map'){
            $jq('th.chart-radio,td.chart-radio').hide();
            $jq('th.map-radio,td.map-radio').show();
          }
        } else if(chartRadios == '1'){
          $jq('th.chart-radio,td.chart-radio').hide();
          $jq('th.map-radio,td.map-radio').hide();
        }
      }
    }//end determineRadioBtns()
    determineRadioBtns();

    function determineFile(){
      if ($jq('input[name=choose-analytics]:checked').val()=='map'){
        setFile("maps");
      } else {
        setFile("charts");
      }
    }

    $jq('.date-range').change(function(){
      determineFile();
    });

    $jq('.choose-analytics-btns').change(function(){
      determineFile();
      determineRadioBtns();
    })
    .change();

  function setFile(whichFile){
    selectedDateRange = $jq('.date-range option:selected').val();
    jsonFile = "/assets/analytics-data-" +whichFile+ ".json?target_month=" + selectedDateRange;
    if (typeof(analytics_url_template) === "string") {
      jsonFile = analytics_url_template
      jsonFile = jsonFile.replace('para_target', whichFile)
      jsonFile = jsonFile.replace('para_target_month', selectedDateRange)
    }
    if(whichFile == 'charts'){
      refreshChartAnalytics();
    } else if (whichFile == 'maps'){
      refreshMapAnalytics();
    }
    return jsonFile;
  }

/******************* JSON data below *******************/
  function refreshChartAnalytics(){
    $jq.getJSON(jsonFile, function(jsonData){

    //Show/Hide analytics
    $jq('#showhide-analytics-btn').toggle(
      function(){
        adjustChartWidth();
      },
      function(){
        //do nothing
      }
    );

    //Data Table Radio Btns
    $jq('.chart-radio').change(function(){
      initCharts();
    });

    //Charts
      //get all charts
      var locationChart = "locationChart";
      var guideChart = "guideChart";
      var eventChart = "eventChart";
      var promotionChart = "promotionChart";
      var bannerChart = "bannerChart";
      var featuredChart = "featuredChart";

      //Responsive Widths
      var chartWidth = 0;
      function adjustChartWidth(){
        chartWidth = $jq('div.container').width() -32;
        initCharts();//redraw charts
      }//end adjustChartWidth()

      var chartTimeOut = null;
      var doAdjustChartWidth = function(){adjustChartWidth()};
      window.onresize = function(){
        if (chartTimeOut != null){
          clearTimeout(chartTimeOut);
        }
        chartTimeOut = setTimeout(doAdjustChartWidth, 100);
      };

      function createChart(htmlSection, jsonSection){
        currentChartID = $jq('input[name=choose-chart-totals]:checked').val();

        //clear old to replace w/new
        $jq('#locationChart, #guideChart, #promotionChart, #eventChart, #bannerChart, #featuredChart').contents().remove();

        //create canvas
        var r = Raphael(htmlSection);

        //empty arrays to hold data
        var x = [],
            y = [],
            datesCount = [],
            dates = [],
            invisibleLine = [],
            views = [],
            clicks = [],
            phoneCalls = [],
            facebookShares = [],
            twitterShares = [],
            emailShares = [],
            whichLines = [];

        if ($jq('#locationChart').length){//if on page
          whichLines = [invisibleLine, views, clicks, phoneCalls];
        } else if ($jq('#guideChart').length){
          whichLines = [invisibleLine, views, clicks];
        } else if ($jq('#promotionChart').length){
          whichLines = [invisibleLine, views, clicks, facebookShares, twitterShares, emailShares];
        } else if ($jq('#eventChart').length){
          whichLines = [invisibleLine, views, clicks];
        } else if ($jq('#bannerChart').length){
          whichLines = [invisibleLine, views, clicks];
        } else if ($jq('#featuredChart').length){
          whichLines = [invisibleLine, views, clicks];
        }

        for(var i=0; i < jsonData.Charts[jsonSection][currentChartID].length; i++){
          dates.push(jsonData.Charts[jsonSection][currentChartID][i].Day);
          datesCount.push(jsonData.Charts[jsonSection][currentChartID][i].DateCount);
          invisibleLine.push(jsonData.Charts[jsonSection][currentChartID][i].InvisibleLine);
          views.push(jsonData.Charts[jsonSection][currentChartID][i].Views);
          clicks.push(jsonData.Charts[jsonSection][currentChartID][i].Clicks);
          phoneCalls.push(jsonData.Charts[jsonSection][currentChartID][i].PhoneCalls);
          facebookShares.push(jsonData.Charts[jsonSection][currentChartID][i].FacebookShares);
          twitterShares.push(jsonData.Charts[jsonSection][currentChartID][i].TwitterShares);
          emailShares.push(jsonData.Charts[jsonSection][currentChartID][i].EmailShares);
        }

        var lines = r.linechart(
          15, //x start in pixels (pushes entire chart to right)
          0, //y start in pixels
          chartWidth, //width
          300, //height of chart in pixels
          datesCount, //amount of x coordinates
          whichLines, //array of y coordinates (lines on chart)
          {//options
            nostroke: false,
            axis: "0 0 1 1",
            axisxstep: datesCount.length-1,//how many x interval labels to render (get amount of dates avail in td from HTML)
            symbol: "circle",
            colors: ["transparent", "#BE6228", "#33312F", "#66625E", "#BFBDBB", "#E79645"]
          }).hoverColumn(function(){
            //adds tags
            this.tags = r.set();
            for (var i = 0, ii = this.y.length; i < ii; i++){
              this.tags.push(r.tag(this.x, this.y[i], this.values[i], 175, 0).insertBefore(this).attr([{fill: this.symbols[i].attr("fill"), stroke: 'none'}, {fill: "#fff"}]));
            }
          }, function(){
            //removes tags
              this.tags && this.tags.remove();
          }
        );

        //Modify X Axis Labels
        var xAxis = lines.axis[0].text.items;//gets current x axis values
        for(var i in xAxis){//iterate through array of xAxis
          xAxis[i].attr({'text': jsonData.Charts[jsonSection][currentChartID][i].Day.toString()});//set text of current element w/all dates
        }
      }//end createChart()

      //create charts when called
      function initCharts(){
        if ($jq('#locationChart').length){//if on page
          createChart(locationChart, 'Locations');//create chart [here] with [this data]
        } else if ($jq('#guideChart').length){
          createChart(guideChart, 'Guides');
        } else if ($jq('#promotionChart').length){
          createChart(promotionChart, 'Promotions');
        } else if ($jq('#eventChart').length){
          createChart(eventChart, 'Events');
        } else if ($jq('#bannerChart').length){
          createChart(bannerChart, 'Banners');
        } else if ($jq('#featuredChart').length){
          createChart(featuredChart, 'FeaturedAds');
        }
      }//end initCharts()
    });//end JSON
  }//end refreshChartAnalytics()

  //Map
  function refreshMapAnalytics(){
    $jq.getJSON(jsonFile, function(jsonData){

      //Data Table Radio Btns
      $jq('.map-radio').change(function(){
        initMaps();
      });

      var map,
          infowindow,
          heatmap,
          heatmapData;
      var currentMapID;
      var whichMap;
      var storeLat,
          storeLng,
          storeLocation;

      //Map Btns
        //views auto selected for map
        $jq('.choose-map-views').addClass('active');
        if ($jq('input[name=choose-map]').length){
          $jq('input[name=choose-map]')[0].checked = true;
        }
      $jq('.choose-map-views').click(function(){
        $jq('.choose-map-clicks').removeClass('active');
        $jq('input[name=choose-map]')[1].checked = false;
        $jq('.choose-map-views').addClass('active');
        $jq('input[name=choose-map]')[0].checked = true;
        initMaps();
      });
      $jq('.choose-map-clicks').click(function(){
        $jq('.choose-map-views').removeClass('active');
        $jq('input[name=choose-map]')[0].checked = false;
        $jq('.choose-map-clicks').addClass('active');
        $jq('input[name=choose-map]')[1].checked = true;
        initMaps();
      });

      function findWhichMap(){
        if($jq('input[name=choose-map]:checked').val()==='Clicks'){
          return "Clicks";
        } else {
          return "Views";
        }
      }//end findWhichMap()

      function createMap(mapLocation, jsonSection){
        //remove data from arrays
        lats = [];
        lngs = [];
        markers = [];
        heatmapData = 0;
        currentMapID = $jq('input[name=choose-map-location]:checked').val();
        whichMap = findWhichMap();

        if(currentMapID == 'allIDs'){
          storeLocation = new google.maps.LatLng(0, 0);
        } else if(currentMapID != 'allIDs'){
          storeLat = jsonData.Maps[jsonSection][currentMapID].Store.Latitude;
          storeLng = jsonData.Maps[jsonSection][currentMapID].Store.Longitude;
          storeLocation = new google.maps.LatLng(storeLat, storeLng);
        }

        for(var i = 0; i < jsonData.Maps[jsonSection][currentMapID][whichMap].length; i++){
          lats.push(parseFloat(jsonData.Maps[jsonSection][currentMapID][whichMap][i].Latitude));
          lngs.push(parseFloat(jsonData.Maps[jsonSection][currentMapID][whichMap][i].Longitude));
          markers.push(new google.maps.LatLng(parseFloat(lats[i]), parseFloat(lngs[i])));
        }

        var mapOptions = {
          minZoom: 2,
          mapTypeId: google.maps.MapTypeId.ROADMAP,
          center: storeLocation
        };
        map = new google.maps.Map(mapLocation, mapOptions);
        heatmapData = new google.maps.MVCArray(markers);
        heatmap = new google.maps.visualization.HeatmapLayer({
          data: heatmapData
        });
        heatmap.setMap(map);
        heatmap.setOptions({
          opacity: .5
        });
        var i = 0;
        var latlngbounds = new google.maps.LatLngBounds();
        for (var i; i < markers.length; i++){
          latlngbounds.extend(markers[i]);
        }
        map.fitBounds(latlngbounds);
      }//end createMap()

      function initMaps(){
        var locationMap = document.getElementById('locationMap'),
            guideMap = document.getElementById('guideMap'),
            promotionMap = document.getElementById('promotionMap'),
            eventMap = document.getElementById('eventMap'),
            bannerMap = document.getElementById('bannerMap'),
            featuredMap = document.getElementById('featuredMap');

        if (locationMap){//if on page
          createMap(locationMap, 'Locations');//create map [here] with [this data]
        } else if (guideMap){
          createMap(guideMap, 'Guides');
        } else if (promotionMap){
          createMap(promotionMap, 'Promotions');
        } else if (eventMap){
          createMap(eventMap, 'Events');
        } else if (bannerMap){
          createMap(bannerMap, 'Banners');
        } else if (featuredMap){
          createMap(featuredMap, 'FeaturedAds');
        }
      }//end initMaps()
      initMaps();
    });//end JSON
  }//end refreshMapAnalytics()

}//end if .analytics on page
});//end document ready

For reference, here's a portion of my analytics-data-maps.json file:


{
	"Maps": {
		"Locations": {
			"allIDs": {
				"Views": [
					{
						"Latitude": 59.32522,
						"Longitude": 18.07002
					},
					{
						"Latitude": 47.6097,
						"Longitude": 122.3331
					}
				],
				"Clicks": [
					{
						"Latitude": 59.32522,
						"Longitude": 18.07002
					}
				]
			},
			"ID1": {
				"Store": {
					"Latitude": 47.6097,
					"Longitude": 122.3331
				},
				"Views": [
					{
						"Latitude": 59.32522,
						"Longitude": 18.07002
					}
				],
				"Clicks": [
					{
						"Latitude": 41.850033,
						"Longitude": -87.6500523
					}
				]
			},
			"ID2": {
				"Store": {
					"Latitude": 59.327383,
					"Longitude": 18.06747
				},
				"Views": [
					{
						"Latitude": 19.4270499,
						"Longitude": -99.1275711
					}
				],
				"Clicks": [
					{
						"Latitude": 59.32522,
						"Longitude": 18.07002
					}
				]
			}
		},
		"Guides": {
			"allIDs": {

It was logical for the UI to only display options when they were available or neeeded, so various UI elements are hidden or shown based on user selections. The radio buttons, for example, are only added to the data table when there are multiple locations available to choose from.

Overall, I'm proud that I was able to attain multiple levels of abstraction with arguments passed through functions and clearing and reusing arrays to switch out JSON data. I learned a lot about JavaScript and programming in building this, and I'm glad I welcomed the challenge when it was presented to me.

Icon Design

I designed various icons for the iPhone, Android, and Web App.

add coupon icon coupons icon edit icon food icon guide icon info icon map icon next icon phone icon share icon shopping icon star icon thumb up icon thumb down icon top 10 icon banner icon business icon click icon email icon event icon facebook icon twitter icon time icon

iPhone & Android Layout Design

Milan had already built several iphone and android apps using their API, so my task was simply to modify existing layouts without changing the flow or app architecture. The primary app I worked on for them was their DAC app.

Shopping Categories:
shopping
Top 10 Deals:
top 10
Event List:
event list
Event:
event
Coupon:
coupon
Featured Ad:
featured ad
Location:
location screen
Select Location:
select location
Empty Coupon Book:
empty coupon book
Out of Area:
out of area screen

Web App Planning: Use Cases & User Goals

There were 3 account types for the portal, and each account type had different permissions. I put together a collaborative spreadsheet to help us determine the functions/roles each account type would have. I broke up the types of users, and we assigned possible tasks that each user type would need to accomplish, which helped us accurately structure the application to meet our user's needs and goals.

Flowcharting & Site Architecture

After discussing user goals and tasks assigned to the different account types, I put together a detailed content inventory and flowchart.

Wireframing & Storyboarding

After the flowcharting and site architecture, I designed the global nav bar to reflect the changes and also did quite a bit of wireframing and storyboarding for other additions to the web app, such as the addition of analytics and potential redesign to accommodate future additions.

Admin Account Type Navigation

admin level 1 navbar admin level 2 navbar admin level 3 navbar

User Account Type Navigation

user level 1 navbar user level 2 navbar

Merchant Account Type Navigation

merchant level navbar

Mobile API Previews in Web App

The primary purpose of the web app was to allow merchants to update their content that appears in the mobile apps. To help merchants connect with that information and see how it will be displayed in the mobile apps, I built a preview with HTML, CSS, and JavaScript. As content is entered in the form on the left, the information is displayed in the preview just as it would in the live mobile apps. Since the mobile API was used in several iPhone and Android apps, it was important to keep the design as generic as possible to make sure individual merchants weren't excluded.

locations section mobile preview guides section mobile preview promotions section mobile preview events section mobile preview