Google Analytics Plugin – Very useful one

Google Analytics (GA) is invaluable when you need to slice and dice web traffic data along numerous dimensions.  For example, by date, time, location just to name few measuring visits, vistiors, pageviews, etc.  For full list of possible combinations take a look at this documentation.

While many business users and SEO/SEM analyts use GA on regular basis to manage their Internet Marketing spend and improve site traffic for better ROI, they can chart a single measure for different segments and analyse one metric at a time.  Many a time users download their data from adwords or get GA data through their in-house API setup.  With almost all business users having used Excel as one of their main analytical tool, they prefer it over other tools (either in-house or 3rd party).  Also, as quickly as things change in Internet Marketing (IM) world, some quick analysis business user can do on their own allows them to respond to quickly to market changes.  You may find bullet 3 and 4 in this article  from Avinash Kaushik relevant.

One freeware plug-in that provides the solution is ExcellentAnalytics’ plug-in for Excel

ExcellentAnalytics’ Plug-in

It is pretty simple to use and an user can start using it immediately.  Once the data that you are interested in is downloaded through this plug-in, you can exploit Excel abilities to further analyze the data without any other (IT) application needed.

After downloading and installing the plug-in, select “ExcellentAnalytics” menu at the top when you bring up MS Excel 2007 and click on “Account” button; then enter your GA email and password for it to connect to GA.  Click on “New Query” and select the profile of interest to you and select valid dimensions and metrics.  If it is an invalid combination or query, the result set does not return any and hopefully future release of this plug-in will provide some user friendly info.

There were queries that didn’t return right result sets. For example, “Visitor Recency” metrics like  “most visits the previous visits happened: X days ago”.  Or under Filter section dimensions are assumed to be string type and any dimension that is a number can’t be entered. For example, you selected “visit count” dimension and want to filter any visit count less than 5.  You can’t.  Hey, it is a freeware and it does a decent job for quick analysis.  Use it with caution and having said that you can always check the numbers against GA’s report to make sure the plug-in didn’t mess up anything.

Sample reports:

HTH,
Shiva

Google NS Individual Search Box

Today, while searching for “cygwin” keyword I was presented with results that caught my eye.  The first result had its own search box.  See picture below.  Had anyone seen this before?  I liked the feature and pretty handy if you are looking for a specific information from a specific site. I entered “latest version” and hit “Search cygwin.com” that showed search entry “latest version site:cygwin.com” in the next search. Nice.


Other posts that may be of interest:
1. Best Google home page logos
2. Better quicker SEM ROI
3. 99 bottles of beer in different laguages


Cheers,
Shiva

Quick ROI for SEM (Adwords) Defined

SEM or Internet paid advertisement or PPC or Adwords or what ever one may call, has been on high growth rate for many years now and has shown that it will continue to grow in the near future with the potential to grab market share from other advertisement channels.  This has implications on all companies that would like to have their products or services advertised.  By quickly responding to the market changes and being nimble, they can effectively spend their marketing dollars with positive ROI.


Though I use Google Adwords in the following explaination, you can replace the same logic for Yahoo and MSN. I assume that you are somewhat familiar with natural search, paid search, clicks, impressions and related; and what they mean.  


Related posts: Quick ROI forecasting technique

Keywords:  
Google or other search engines provide you the service to bid for specific keywords/ phrases.  And at the same time they track those keywords performance including clicks, impressions, and many more.  The big advantage is the availability of all this information through APIs (Application Programming Interfaces) and  I suggest you use daily summary to help track them.  The APIs calls are simple to setup and you can get it going with mainly one report called “Placement/ Keyword Performance Report”.
Google takes less than 1/2 an hour to provide the 80K to 100K keyword data. On making the API call to get the report, make the application check every minutes or so and once ready pull the report.  If you are pulling the reports from more than one search engine, it is easy to have separate instances of application and then merge everything in database or data warehouse.  This way you can have better concurrency.
Application:
This is the work horse that pulls the data from search engines.  The application can be single threaded to handle small load or if it is a large load (say, more than 100K keywords), you can use multi-threaded programming techniques.   Google’s v13 or v2009 API calls provide all the important data elements that one need, but in case of Yahoo you need to make two separate calls if you need max bid and keyword type.  But for most business uses you can do without them thus simplifying the process. The complexity araises not because you need to make 2nd call, but Yahoo returns different matching data and you need to take care that you match them to the correct bidding units.  I will provide more information on this in other articles.
Psudeocode or logic:
   Authenticate with Adwords (with your token)
   Make a SOAP request (through Perl, Java, PHP, etc.)
   Dump all the data to a file (Google has more than 50 columns).  Also, do any changes or cleanup, if needed.

Data Warehouse:
Once the data is dumped into file, have another process or same process bulk load the data into staging area of the data warehouse.  Medium or large companies typically have their own internal process setup to track the conversion and revenue.  So, by combining the spend/cost data from Google with your revenue, you can measure the ROI very effectively to the lowest level of granularity – keyword and its type.  You can also slice and dice the data along time, accounts, campaigns, ad groups, geo-location, etc.
Psuedocode or logic:
     Validate staging data
     Join revenue data with cost/spend data
     Summarize with time, bid unit, location, search engines, sites and other dimensions
Bidding:
Once you have the metrics of interest to you (either positive ROI, rate of improvement in clicks/ impressions, etc.), you can create reports for only those keywords/ campaigns that of interest to you.  Now you are ready to increase or decrease the bid amount on the keyword.  Also, note in this step, you should optimize your pages to the keywords so that your quality score is improved.  Just increasing the bid without the well optimized page is throwing the money away.
Psuedocode or logic:
   If daily change to number of keywords is not high, you can do bid changes manually or through excel.
   Else build automated bid framework that helps you
Cheers,

Shiva

Useful SEM SEO Links

Google Analytics Data Retriever (API) – SEM, SEO

Google Analytics should be an integral part of any company’s marketing data analysis strategy that plays in SEM, SEO field. Not too long ago Google released API for their Analytics and one can use this data or reports to compliment their in-house data analysis.

Here I have provided a small utility program that can be used to retrieve most popular six (6) reports and have used similar utilities that I have built to dump the data into MySQL data warehouse for much bigger data sets.

The application has two Perl modules (GAProfile.pm and GAReport.pm) and a main program (ga_report_retriever.pl). GAProfile.pm module is used to authenticate your account and get the auth_code that will be used in the subsequent calls to GA to get reports. GAReport.pm module is used to pull the report and dump important dimension and metrics to standard output.  Put both these modules in the same directory (or copy them to directory in the path of PERL5LIB) as the main program and run it. To get different report, make a change in “ga_report_retriever.pl” passing the name of the report.

Before you start using the GA API, you may want to go through the documentation.

At command prompt> ./ga_report_retriever.pl


GAProfile.pm module

package GAProfile;
#-------------------------------------------------------
#- This is a Google Analytics Class providing methods to
#- 1. get authorized with google accounts &
#- 2. get list of profiles to pull analytics data
#- Use XML::Simple module and for large data sets, you can
#- try XML::Bare or LibXML modules for better performance.
#- Provide your own Google account info in the contructor.
#-
#- Author: Shiva M.
#-------------------------------------------------------

use strict;
use XML::Simple;
use URI::Escape;
use LWP::UserAgent;

use Data::Dumper;

#--- Create Google Analytics object.
#-------------------------------------------------------
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};

$self->{ga_user} = '{your google account}';
$self->{ga_passwd} = '{corresponding password}';

#-- Client Login URL
$self->{ga_login_url} = 'https://www.google.com/accounts/ClientLogin';
$self->{ga_login_param} = "accountType=GOOGLE";
$self->{ga_login_param} .= "&Email=$self->{ga_user}&Passwd=$self->{ga_passwd}";
$self->{ga_login_param} .= "&service=analytics&source=HiRegion.com-GA_Retriever-v1";

#-- Analytics account URL
$self->{ga_ac_url} = "https://www.google.com/analytics/feeds/accounts/default";

$self->{ga_auth_code} = undef;
$self->{ga_ua_name} = "GA Data Retriever"; #-- LWP User Agent name

bless ($self, $class);
return $self;
}

#- Escape the URI characters, if needed.
#-------------------------------------------------------
sub urlEncode {
my $self = shift;
my $str = shift;

return uri_escape($str)
}

#-- Get Auth code from Google Analytics so that you can make
#-- Data request calls. This sets object's ga_auth variable as well.
#-------------------------------------------------------
sub getAuthorized {
my $self = shift;

my $r = HTTP::Request->new(POST => $self->{ga_login_url});
$r->content_type('application/x-www-form-urlencoded');
$r->content($self->{ga_login_param});

#-- Make a call to client login...
my $ua = LWP::UserAgent->new(agent => $self->{ga_ua_name});
my $res = $ua->request($r);

my ($ga_auth);
if ($res->is_success) { #-- On success returns HTTP 200.
$ga_auth = (split(/Auth=/i, $res->content))[1];
#- print "content: ", $res->content(), "\nga_auth: $ga_auth\n";
}
else {
return (undef, "error: ". $res->status_line);
}

$self->{ga_auth_code} = $ga_auth;
# return the token
return (1, $ga_auth); #-- Return success & auth code
}

sub getAuthCode {
my $self = shift;
return $self->{ga_auth_code};
}


#-- Get list of profile IDs (web property IDs)
#-------------------------------------------------------
sub getProfiles {
my $self = shift;

# create user agent object & get profiles...
my $ua = LWP::UserAgent->new(agent => $self->{ga_ua_name});
my $res = $ua->get($self->{ga_ac_url}, Authorization => "GoogleLogin Auth=$self->{ga_auth_code}");

my @profiles;

# On getting the list of profiles...
if ($res->is_success) {
my ($content, $e);
$content = $res->content;

# Parse the xml response...
my $xml = new XML::Simple(KeyAttr=>[]);
my $tree = $xml->XMLin($content);

# iterate through each entry
my $ii = 0;
foreach $e (@{$tree->{entry}}) {
# add the account to the array
$profiles[$ii]->{title} = $e->{title}->{content};
$profiles[$ii]->{tableId} = $e->{'dxp:tableId'};

foreach my $p (@{$e->{'dxp:property'}}) {
if ($p->{'name'} =~ m/accountId/i) {
$profiles[$ii]->{accountId} = $p->{'value'};
} elsif ($p->{'name'} =~ m/profileId/i) {
$profiles[$ii]->{profileId} = $p->{'value'};
} elsif ($p->{'name'} =~ m/webPropertyId/i) {
$profiles[$ii]->{webPropertyId} = $p->{'value'};
}
}
$ii++;
}
} else {
# Request failed.
print "GAProfile->getProfiles() error: ". $res->status_line, "\n";
return undef;
}

return @profiles;
}


1;

GAReport.pm module


package GAReport;
#------------------------------------------------------
#- Google Analytics Report Module.
#- Handles url request to six popular pre-defined reports
#-
#- Author: Shiva M.
#------------------------------------------------------
use Date::Calc qw(:all);
use Data::Dumper;

sub new {
my $proto = shift;
my $profileId = shift;
my $class = ref($proto) || $proto;
my $self = {};

$self->{ga_data_url} = "https://www.google.com/analytics/feeds/data?";
$self->{standard_reports} = { visit_pageviews => visit_pageviews
,visit_traffic => visit_traffic
,visit_browser => visit_browser
,language_country => language_country
,top_pageviews => top_pageviews
,n_days_visits => n_days_visits
};
$self->{current_report} = "visit_traffic"; #-- Default report.
bless ($self, $class);

$self->{'profileId'} = undef;

return $self;
}


#-- Sets the profile id for data feed
#---------------------------------------------------------------------
sub setProfileId {
my $self = shift;
my $profileId = shift;

if (!defined $profileId or $profileId !~ m/\d+/) {
print "GAReport-setProfileId-Error: Ill formed profileID ($profileId)\n";
return undef;
}
$self->{profileId} = $profileId;

return 1;
}


#-- Returns current profile ID url param.
#---------------------------------------------------------------------
sub getProfileId {
my $self = shift;
return "ids=ga:$self->{profileId}"; #-- Append "ga:profileId"
}


#-- Most important reports are defined as standard reports and
#-- need to be predefined in constructor. Currently supports six reports.
#---------------------------------------------------------------------
sub getStandardReportURL {
my $self = shift;
my $report = shift; my $startDt = shift; my $endDt = shift; my $maxRes = shift;
#- my ($report, $startDt, $endDt, $maxRes) = @_;

my $url = $self->{ga_data_url};
$url .= $self->getProfileId();
$url .= $self->getDateDim($startDt, $endDt);
$url .= $self->getMaxResultDim($maxRes);

if ($report =~ m/$self->{standard_reports}->{visit_pageviews}/i) {
$url .= "&dimensions=ga%3Adate&metrics=ga%3Avisits%2Cga%3Apageviews";
} elsif ($report =~ m/$self->{standard_reports}->{visit_traffic}/i) {
$url .= "&dimensions=ga%3Asource%2Cga%3Amedium&metrics=ga%3Avisits&sort=-ga%3Avisits";
} elsif ($report =~ m/$self->{standard_reports}->{visit_browser}/i) {
$url .= "&dimensions=ga%3Abrowser&metrics=ga%3Avisits&sort=-ga%3Avisits";
} elsif ($report =~ m/$self->{standard_reports}->{language_country}/) {
$url .= "&dimensions=ga%3Alanguage%2Cga%3Acountry&metrics=ga%3Avisits&sort=-ga%3Avisits";
} elsif ($report =~ m/$self->{standard_reports}->{top_pageviews}/i) {
$url .= "&dimensions=ga%3ApagePath%2Cga%3ApageTitle&metrics=ga%3Apageviews&sort=-ga%3Apageviews";
} elsif ($report =~ m/$self->{standard_reports}->{n_days_visits}/i) {
$url .= "&dimensions=ga%3Adate&metrics=ga%3Avisits&filters=ga%3Avisits%3E1&sort=ga%3Adate";
} else {
print "GAReport.pm-getStandardReport-Error: Unknown standard report\n";
$url = undef;
}

#-- Which report are we requesting...
$self->{current_report} = $report;

return $url;
}


sub isDateDimSet {
my $self = shift;
my $url = shift;

if ($url =~ m/start-date=\d{4}\d{2}\d{2}/i and $url =~ m/end-date=\d{4}\d{2}\d{2}/i) {
return 1;
}
return undef;
}


#-- Returns date parameters of URL. If undefined dates, then sets them to a week before.
#-- i.e; "&start-date=2009-10-02&end-date=2009-10-09"
#------------------------------------------------------
sub getDateDim {
my $self = shift;
my $startDt = shift; #-- Start Date
my $endDt = shift; #-- End Date

my ($y, $m, $d, $dtURL); #-- Year, Month, Date
if (!defined $startDt or !defined $endDt) {
($y, $m, $d) = Today();
($y, $m, $startDt) = Add_Delta_Days($y, $m, $d, "-9"); #-- A week before day-before-yesterday. Allow 2 days lag.
($y, $m, $endDt) = Add_Delta_Days($y, $m, $startDt, "7"); #-- Add 7 days to start date to get a week's report.
$dtURL = "&start-date=$y-$m-$startDt&end-date=$y-$m-$endDt";
} else {
$dtURL = "&start-date=$startDt&end-date=$endDt";
}

return $dtURL;
}


#-- Returns maximum number of resutls requested from GA.
#-- Defaults to 50.
#--------------------------------------------------------
sub getMaxResultDim {
my $self = shift;
my $maxRes = shift;

if (!defined $maxRes or !$maxRes =~ m/\d+/) {
$maxRes = 50;
}
return "&max-results=$maxRes"
}


#-- Before making data request some sanity check...
#--------------------------------------------------------
sub validateRequest {
my $self = shift;

return 1;
}


#-- Prints out the dimensions and metrics of a report.
#--------------------------------------------------------
sub getReport {
my $self = shift;
my $url = shift;
my $ga_auth_code = shift;

#- print "\n\nGAReport-getReport: Pulling $self->{current_report} now.";

if (!defined $url or !defined $ga_auth_code) {
print "GAReport-getReport-Error: url($url)) or authorization code ($ga_auth_code) is missing.\n";
return undef;
}
#-- Construct user agent and make a get call.
my $ua = LWP::UserAgent->new;
$ua->agent("GA Test Data Retriever/0.5");

my $res = $ua->get($url, Authorization => "GoogleLogin Auth=$ga_auth_code");

my $data;
#-- Check response and get the data...
if ($res->is_success) {
$data = $res->content;
} else {
print "GAReport-getReport-Error: Data pull failed: ". $res->status_line;
return undef;
}

#-- Convert XML to Perl tree structure and parse.
#------------------------------------------------
my $xml = XML::Simple->new();
my $tree = $xml->XMLin($data);
print Dumper $tree;

my $tableName = $tree->{'dxp:dataSource'}->{'dxp:tableName'};
print "\n\nTablename: $tableName";

foreach my $ok (keys %$tree) { #-- Outermost hash keys
#- print "Tree key: $ok\n";
#- my $tableName = $ok->{'dxp:tableName'};


#-- Pageview & visits metrics
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/visit_pageviews/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

#-- print "\n\tentries: @hEntries\n";
printf "\n\n%-25s\t%s\t\t%s\t%s\n%s", "Profile", "Date", "PageViews", "Visits", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $dt = $hEAttr->{'dxp:dimension'}->{'value'};
$out{$dt}{'pageviews'} = $hEAttr->{'dxp:metric'}->{'ga:pageviews'}->{'value'};
$out{$dt}{'visits'} = $hEAttr->{'dxp:metric'}->{'ga:visits'}->{'value'};
}

foreach my $dt (sort keys %out) {
printf ("\n%-25s\t%d\t%d\t\t%d", $tableName, $dt, $out{$dt}{'pageviews'}, $out{$dt}{'visits'});
}
}


#-- Visitor traffic metrics
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/visit_traffic/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

printf "\n\n%-25s\t%-20s\t%-20s\t%s\n%s", "Profile", "Source", "Medium", "Visits", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $src = $hEAttr->{'dxp:dimension'}->{'ga:source'}->{'value'};
my $medium = $hEAttr->{'dxp:dimension'}->{'ga:medium'}->{'value'};
$out{$src}{$medium}{'visits'} = $hEAttr->{'dxp:metric'}->{'value'};
}

foreach my $src (sort keys %out) {
foreach my $med (sort keys %{$out{$src}}) {
printf ("\n%-25s\t%-20s\t%-20s\t%d", $tableName, $src, $med, $out{$src}{$med}{'visits'});
}
}
}


#--- Visit and browser metrics
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/visit_browser/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

printf "\n\n%-25s\t%-20s\t%s\n%s", "Profile", "Browser", "Visits", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $brow = $hEAttr->{'dxp:dimension'}->{'value'};
$out{$brow}{'visits'} = $hEAttr->{'dxp:metric'}->{'value'};
}

foreach my $brow (sort keys %out) {
printf ("\n%-25s\t%-20s\t%d", $tableName, $brow, $out{$brow}{'visits'});
}
}


#--- Language and country metrics...
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/language_country/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

printf "\n\n%-25s\t%-20s\t%-20s\t%s\n%s", "Profile", "Language", "Country", "Visits", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $lang = $hEAttr->{'dxp:dimension'}->{'ga:language'}->{'value'};
my $country = $hEAttr->{'dxp:dimension'}->{'ga:country'}->{'value'};
$out{$lang}{$country}{'visits'} = $hEAttr->{'dxp:metric'}->{'value'};
}

foreach my $lang (sort keys %out) {
foreach my $country (sort keys %{$out{$lang}}) {
printf ("\n%-25s\t%-20s\t%-20s\t%d", $tableName, $lang, $country, $out{$lang}{$country}{'visits'});
}
}

}


#--- Top page views metrics...
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/top_pageviews/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

printf "\n\n%-25s\t%-30s\t%-20s\t%s\n%s", "Profile", "PagePath", "PageTitle", "PageViews", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $pagePath = $hEAttr->{'dxp:dimension'}->{'ga:pagePath'}->{'value'};
my $pageTitle = $hEAttr->{'dxp:dimension'}->{'ga:pageTitle'}->{'value'};
$out{$pagePath}{$pageTitle}{'visits'} = $hEAttr->{'dxp:metric'}->{'value'};
}

foreach my $pagePath (sort keys %out) {
foreach my $pageTitle (sort keys %{$out{$pagePath}}) {
printf ("\n%-25s\t%-30s\t%-20s\t%d", $tableName, $pagePath, $pageTitle, $out{$pagePath}{$pageTitle}{'visits'});
}
}

}


#--- N days of visits...
#-----------------------------------------------------------------
if ($self->{current_report} =~ m/n_days_visits/i and $ok =~ m/entry/i) {
my @hEntries = values %{$tree->{$ok}}; #-- hash refs

printf "\n\n%-25s\t%-20s\t%s\n%s", "Profile", "Date", "Visits", '-'x80;

my %out = ();
foreach my $hEAttr (@hEntries) {
my $dt = $hEAttr->{'dxp:dimension'}->{'value'};
$out{$dt}{'visits'} = $hEAttr->{'dxp:metric'}->{'value'};
}

foreach my $dt (sort keys %out) {
printf ("\n%-25s\t%-20s\t%d", $tableName, $dt, $out{$dt}{'visits'});
}

}
}

return 1;
}

1;

Main Program: ga_report_retriever.pl


#!/usr/local/bin/perl -w
#---/cygdrive/c/Perl/bin/perl -w

#-----------------------------------------------------------
#- Application to pull 6 popular Google Analytical reports.
#- Uses two classes - GAProfile and GAReport to perform data/report pull through APIs.
#- Reports are:
#- Pageview visits by date (visit_pageviews)
#- Visit by by source (visit_traffic)
#- Visit by brower (visit_brower)
#- Visit by language, country (language_country)
#- Top pageviews (top_pageviews)
#- "n" day visits (n_days_visits)
#-
#- Author: Shiva M.
#-----------------------------------------------------------
use lib ".";
use strict;
use Data::Dumper;

use GAProfile;
use GAReport;

#-- Get authroized to pull data. Pass username & password through GAProfile.
#-----------------------------------------------------------
my $gau = GAProfile->new();
$gau->urlEncode;
#-- $rc - return code 1 on success & set $ga_auth to authorization code.
my ($rc, $ga_auth) = $gau->getAuthorized();

#-- If failed, terminate the program.
if (!$rc) {
print "Authorization failed. Exiting. Fatal error: $ga_auth\n";
die;
}

#-- On success, get list of profiles to pull data from and try one profile at a time.
#------------------------------------------------------------
my @profiles = $gau->getProfiles();

my $report = GAReport->new();
foreach my $p (@profiles) {
$report->setProfileId($p->{profileId});

#-- Get a report by date for last 7 days by default.
#-- If n_days_visits report pass start & end dates and number of days between.
my $url = $report->getStandardReportURL("n_days_visits", '2009-12-01', '2009-12-30', 30);
$report->getReport($url, $ga_auth);
}

Google Docs Spreadsheet – Better access control needed

I use Google docs to maintain many documentations for a non-profit organization and this has been a very useful, money & time saving application. In my view, following three features will immediately enhance what is already a excellent collaborative product.  These are low-hanging fruits.

  • User access control at worksheet level to view
  • Copying more than 1000 (one thousand cells) at a time
  • Multi-column sorting

In our non-profit organization we have multiple chapters controlling their own documents/ spreadsheets and some times there are situations where I would like to have a single spreadsheet but each work sheet is visible only to a particular chapter or member. For example, a common spreadsheet with 50 worksheets (one for each US state) should appropriately be visible to these chapters.  A California chapter can view California worksheet only and not others.

Currently, I can control the editing access through “Protect Sheet” but not the viewing. See below.

Google, could you please extend the feature to control the viewing along with editing?

Another feature that would be very useful would be copy more than 1000 cells at a time from one spreadsheet to another.   If a worksheet has 20 columns and 250 rows and if one likes to duplicate this data with copy and paste (^C & ^V), she would need 5 attempts :(.

Google, could you remove this limitation?

And finally, sorting by more than one column.  Currently one can sort by a selected (single) column only. Though, this is not a major hindrance but by providing multi-column sorting feature I can avoid bringing data to Excel when I need to sort the data in multiple ways.

Thanks,

Best of Google homepage logos over the years

Google modifies their home page by including birthdays of some major personalities around the world or major events. Here are some of the best, unique and presented in more than one country. The list below is not any particular order.

2009.05.20 – Charles Darwin Birthday

2009.11.13 – Water on Moon found

2009.10.02 – Mahatma Gandhi

1998.10.29 – Google Beta

1998.08.30 – Burning Man (One of the first of Google’s modified logos!)

2006.01.04 – Louis Braille’s Birthday

2008.05.16 – Laser Invention

2009.10.07 – Barcode Invention

2003.03.14 – Einstein’s Birthday

2006.02.11 – Olympics Snowboarding

2007.04.22 – Earth Day

2005.04.15 – Leonardo Da Vinci Birthday

2008.11.04 – US Election Day

2009.05.20 – Darwinius Masillae

2009.01.28 – Jackson Pollock Birthday

2002.05.20 through 24 – Dilbert Comics

 

2005.03.30 – Van Gogh’s Birthday

2006.12.12 – Edvard Munch Birthday

2008.06.06 – Diego Velazquez’s Birthday

2007.04.01 – April Fool Day   — (TiSP)

2009.02.12 – Samuel Morse Birthday

2004.02.03 – Gaston Julia’s Birthday

Fractals 

2009.10.29 – Asterix Comic’s 50th Anniversary

2008.01.28 – 50th Annivesary of the LEGO Brick

2009.10.31 – Happy Halloween

2008.01.01 – 25 Years of TCP/IP protocol