Kuznetsov
Kuznetsov
a UI redesign prioritizing responsiveness and user-friendliness
view online >a text visualization of data from The PRIDE Study, commissioned by Ibis Reproductive Health
view online >I designed and coded an online data visualization of 5,000 responses to the PRIDE Study, in which trans and gender non-conforming respondents describe their gender identities in words and share their demographic information. I toyed with color schemes and font choices in order to meet my client’s goal of building an approachable, fun, and user-friendly design tailored to an LGBT, non-expert target audience.
I used jQuery to make visualizations of quantitative data related to respondent race, region, and age. The more complicated task was to clean, consolidate, and organize the qualitative data, which consisted of thousands of strings of free-write responses. The final product includes a search function which allows users to filter the display by respondent age, region, and gender identity. It also features a “keyword” search allowing users to view common themes in the text data.
In this project, I used Javascript/jQuery for a wide range of purposes, including cleaning a data set, generating dynamic HTML elements with data attributes, and producing visual effects around user interactions.
Being presented with a dataset of CSV strings and respondent demographic information, I first reorganized the data into a Javascript array of unique responses while preserving the unique demographic information of each individual respondent.
Transgender man,Cisgender man,Transgender woman,Cisgender woman,Genderqueer,Non-binary,Agender,Two-spirit,Two-spirit_write-in,Additional gender,Additional gender_write-in,Gender id prefer not to say,Man,Woman,Sex assigned at birth,SAAB-Writein,intersex,AIAK native,Black-AfricanAmerican,Hispanic Latinx,Middle East North African,Native Hawaiian Pacific Islander,White,Unknown race,Other,East Asian,South Asian,Southeast Asian,Central Asian,None of these races,newregion,age_new
1,,,,,,,,,,,,,,1,,0,,,,,,1,,,,,,,,Northeast,39
1,,,,1,1,,,,,,,,,1,,0,,,,,,1,,,,,,,,Northeast,30
,,,,,1,,,,,,,,,1,,0,,,,,,1,,,,,,,,Northeast,33
1,,,,,1,,,,1,Bigender,,,,1,,0,,,,,,1,,,,,,,,Northeast,31
,,,,,,,,,1,transmasculine,,,,1,,0,,,,,,1,,,,,,,,Northeast,37
,,,,1,1,1,,,1,genderfluid,,,,1,,0,,,,,,1,,,,,,,,Northeast,24
…,[
"non-binary femme", //unique text response
6, //number of respondents
"0023G0000000010S100Re1000,0032G0000000010S100Re1000,00NAG0000000010S100Re0010,00NAG0000100010S100Re0001,00NAG0000000010S100Re0001,00NAG0000100010S100Re0010" //consolidated demographic info of each
],
[
"still in the works, i guess. i like the term gender non-conforming.",
1,
"0020G0000101010S100Re1000"
],…
$(function(){
var unique_labels = [],
tempKey = "",
tempLabel = "";
for (let i = 0; i < keys_array.length; i++) //for each key
{
if (unique_labels.indexOf(keys_array[i][0]) == -1) //if this key.colA does not match any entries in the "unique labels" array
{
unique_labels.push(keys_array[i][0]); //put it.colA in the unique labels array
}
else //that means the current key is a duplicate
{
var tempLabel = keys_array[i][0], // templabel = duplicate key.colA
tempKey = keys_array[i][2], // tempKey = duplicate key.colC
firstInstance; // placeholder
// the goal is to find where the first occurence of duplicate key.colA was
for (var j = 0; j < i; j++) //for each key before the duplicate key
{
if (keys_array[j][0] == tempLabel) //if we found a match for duplicate key.colA
{
firstInstance = j; // we note its index in the array as firstInstance
var colC = keys_array[firstInstance][2]; // we go to that position and get its colC, a string
colC = colC + "," + tempKey; // we add the duplicate key's colC
keys_array[firstInstance][2] = colC; // we update the value of the original instance
}
}
keys_array.splice(i, 1); //remove duplicate key from array of all keys so that the code does not encounter it again. the length of keys array goes down by 1 each time we do this
i--; //now there is a new key at position i so we offset the i++ at the end of this loop
}
}
});
Having reorganized the data, I then combined jQuery and HTML data attributes to assign search "IDs" to each element.
function assign_searchID() //make data attributes for each
{
$('main div:not(.sex)').each(function(){ //exclude elements that display biological sex rather than gender
var string = $(this).text(),
entries = keys_array.length; //keys_array contains all data
for (let i = 0; i < entries; i++)
{
if (keys_array[i][0] == string)
{
//it's a match!
var temp = keys_array[i][2];
}
}
$(this).attr('searchid', temp);
});
};
Finally, the following code allows users to filter text responses by demographic data, according either to an exclusive or inclusive search.
function filter_results_by(s)
{
var searchkey = s,
searchID;
$('main div').each(function(){
if ($(this).attr('searchid'))
{
searchID = $(this).attr('searchid');
if (searchID.indexOf(',') > -1) //if there are multiple searchIDs
{
$(this).addClass('filtered_out');
searchIDArrays = searchID.split(",");
for (let i = 0; i < searchIDArrays.length; i++)
{
if (exclusive)
{
if ((compare_keys_exclusive(s, searchIDArrays[i])))
{
$(this).removeClass('filtered_out');
}
}
else
{
if ((compare_keys_inclusive(s, searchIDArrays[i])))
{
$(this).removeClass('filtered_out');
}
}
}
}
else //if there is 1 searchID
{
if (exclusive)
{
if (!(compare_keys_exclusive(s, searchID)))
{
$(this).addClass('filtered_out');
}
}
else
{
if (!(compare_keys_inclusive(s, searchID)))
{
$(this).addClass('filtered_out');
}
}
}
}
else
{
$(this).addClass('impossible_to_tell_if_match');
}
});
$('.filtered_out').hide();
$('.impossible_to_tell_if_match').hide();
$('main').prepend('<div id="banner">The following results match your search:</div>')
}
function compare_keys_exclusive(a, b)
{
var filter = a,
searchID = b; // true = do not filter out, false = filter out
var age = searchID.substring(2,4),
minAge = parseInt(filter.substring(0,2)),
maxAge = parseInt(filter.substring(2,4)),
ageFiltered = !(minAge == 15 && maxAge == 99);
var gender = searchID.substring(5,15),
genderFilter = filter.substring(5,15),
genderFiltered = !(genderFilter == 1111111111),
sex = searchID.substring(16,19),
sexFilter = filter.match(/(?<=S)(.*?)(?=Re)/).pop(),
sexFiltered = !(sexFilter == 111),
region = searchID.substring(21,25),
regionFilter = filter.match(/(?:[^Re])+$/).pop(),
regionFiltered = !(regionFilter == 1111);
var agetrue,
regiontrue,
gendertrue;
if (ageFiltered)
{
if (age == "NA")
{
agetrue = false;
}
else if(age < minAge || age > maxAge)
{
agetrue = false;
}
else
{
agetrue = true;
}
}
else
{
agetrue = true;
}
if (regionFiltered)
{
if (region == "NA")
{
regiontrue = false;
}
else
{
regiontrue = compare_strings(region, regionFilter);
}
}
else
{
regiontrue = true;
}
if (genderFiltered)
{
if (gender == "NA")
{
gendertrue = false;
}
else
{
gendertrue = compare_strings(gender,genderFilter)
}
}
else
{
gendertrue = true;
}
if (agetrue && regiontrue && gendertrue)
{
return true;
}
}
function compare_keys_inclusive(a, b)
{
var filter = a,
searchID = b; // true = do not filter out, false = filter out
var age = searchID.substring(2,4),
minAge = parseInt(filter.substring(0,2)),
maxAge = parseInt(filter.substring(2,4)),
ageFiltered = !(minAge == 15 && maxAge == 99);
var gender = searchID.substring(5,15),
genderFilter = filter.substring(5,15),
genderFiltered = !(genderFilter == 1111111111),
sex = searchID.substring(16,19),
sexFilter = filter.match(/(?<=S)(.*?)(?=Re)/).pop(),
sexFiltered = !(sexFilter == 111),
region = searchID.substring(21,25),
regionFilter = filter.match(/(?:[^Re])+$/).pop(),
regionFiltered = !(regionFilter == 1111);
// age
if (ageFiltered)
{
if (age == "NA")
{
return false;
}
else if(age < minAge || age > maxAge)
{
return false;
}
}
// region
else if (regionFiltered)
{
if (region == "NA")
{
return false;
}
else if(!(compare_strings(region, regionFilter)))
{
return false;
}
}
// gender
else if (genderFiltered)
{
if (gender == "NA")
{
return false;
}
else if(!(compare_strings(gender,genderFilter)))
{
return false;
}
}
return true;
}
function compare_strings(a, b)
{
// a is the ID, b is the filter. tell me if it's an exact match, partial match, or no match
var length = a.length,
match = false;
if (a == b)
{
match = true;
// exact match;
}
else
{
for (let i = 0; i < length; i++)
{
if (a[i] == 1 && b[i] == 1)
{
match = true;
//partial match
}
}
}
return match;
}
a website, logos, and social media ads for New York City’s last Yiddish bookstore in Queens, NY. built on Squarespace.
the site is preserved privately and password protected. use password books to view.
view online >a portfolio for writer Sachiko Ragosta
view online >I built a website for the writer Sachiko Ragosta. It houses their biography, portfolio, and “The Mythology of Blood and Boyhood”, a short story turned into an interactive, multimedia creative non-fiction chapbook. The site is inspired by the recurring themes of Ragosta’s work, such as fragmentation, strage futures, and identity.
<section id="about">
<div class="a colora flex"></div>
<div class="b colora flex"></div>
<div class="c colora flex"></div>
<div class="d colora flex"></div>
<div class="1 colorb flex"></div>
<div class="2 colorb flex"></div>
<div class="3 colorb flex"></div>
<div class="4 colorb flex"></div>
<div id="box">
<div class="ta colorme"></div>
<div class="sa colorme"></div>
<div class="ka colorme"></div>
<div class="ti colorme"></div>
<div class="si colorme"></div>
<div class="ka colorme"></div>
<div class="ti colorme"></div>
<div class="si colorme"></div>
<div class="ka colorme"></div>
<div class="tu colorme"></div>
<div class="su colorme"></div>
<div class="ki colorme"></div>
<div class="innerka">
<div class="ka1 colorme"></div>
<div class="ka2 colorme"></div>
<div class="ka3 colorme"></div>
<div class="ka4 colorme"></div>
<div class="ka5 colorme"></div>
<div class="ka6 colorme"></div>
</div>
</div>
</section>
CSS
/* grid insanity */
main
{
width: 100%;
margin: 0 auto;
text-align: center;
color: #1e1e1e;
background-color: #E3B505;
}
#about
{
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 40px auto 40px;
grid-template-rows: 40px auto 40px;
grid-template-areas:
"a 1 b"
"2 box 3"
"c 4 d";
gap: 5px 5px;
}
#about div.flex
{
display: flex;
align-items: center;
justify-content: center;
}
/* border area */
#about div.colora
{
background-color: #8F0000;
}
#about div.colorb
{
background-color: #E5EBF0;
}
#about .a
{
grid-area: a;
}
#about .b
{
grid-area: b;
}
#about .c
{
grid-area: c;
}
#about .d
{
grid-area: d;
}
/* box for index and about*/
#about #box
{
grid-area: box;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 2.5fr 2.5fr 2fr;
grid-template-areas:
"ta sa ka"
"ti si ka"
"ti si ka"
"tu su ki";
gap: 5px 5px;
}
#about #box .ta
{
grid-area: ta;
}
#about #box .ta, #about #box .ti
{
text-align: right;
font-weight: 800;
font-size: 35px;
padding: 0px 15px;
}
#about #box .si
{
background-color: #a94d50 !important;
color: #E5EBF0;
letter-spacing: 2px;
}
#about #box .sa
{
grid-area: sa;
}
#about #box .ka
{
grid-area: ka;
}
#about #box .innerka
{
grid-column: 3 / 4;
grid-row: 1 / 4;
background-color: #E3B505;
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
grid-template-areas:
"ka1 ka2"
"ka3 ka4"
"ka5 ka6";
gap: 5px 5px;
perspective: 60px;
}
.transformed
{
transform-style: preserve-3d;
}
#about #box .innerka .ka1
{
grid-area: ka1;
}
#about #box .innerka .ka2
{
grid-area: ka2;
}
#about #box .innerka .ka3
{
grid-area: ka3;
}
#about #box .innerka .ka4
{
grid-area: ka4;
}
#about #box .innerka .ka5, #about #box .innerka .ka5 a
{
grid-area: ka5;
font-family: 'Hiragino Kaku Gothic ProN', sans-serif;
color: #822b2e;
text-decoration: underline;
font-weight: bold;
text-align: left;
font-size: 14px;
}
#about #box .innerka .ka5 p
{
position: relative;
top: 70px;
padding: 5px 10px 5px 10px;
display: block;
}
#about #box .innerka .ka6
{
grid-area: ka6;
}
#about #box .ti
{
grid-area: ti;
}
#about #box .si
{
grid-area: si;
padding: 15px 5px;
text-align: left;
font-size: 17px;
}
#about #box .ki
{
grid-area: ki;
padding: 15px 5px;
text-align: left;
font-size: 17px;
white-space: pre-wrap;
}
#about #box .tu
{
grid-area: tu;
}
#about #box .su
{
grid-area: su;
}
jQuery
var colors = ['4d367f', 'fd7bab', 'ff898b', '29254d', '793c50', '523678', '8972a3','d8678f','40353f', '1a0546', '4e6325', 'a96141', '381864', '516288', '804b74', '341736', '7a0b50', '0a320b', '021d29', '5f0df0', '470b13', '732431', 'cf557e', '815aa9'];
var colored = false;
var portrait = false;
$('#about #box .colorme').each(function(){
var color = colors[Math.floor(Math.random() * colors.length)];
var color2 = colors[Math.floor(Math.random() * colors.length)];
$(this).css({
'background': '#' + color,
'background': 'linear-gradient(90deg, #'+ color + ', #' + color2 + ')'
});
});
$('#about #box .colorme').hover(function(){
if (!colored)
{
var color = colors[Math.floor(Math.random() * colors.length)];
var color2 = colors[Math.floor(Math.random() * colors.length)];
h = $(this).innerHeight / 3;
deg = 45 * Math.floor(Math.random() * 7);
$(this).css({
'background': '#' + color,
'background': 'linear-gradient(' + deg + 'deg, #'+ color + ', #' + color2 + ')',
'border': h + 'px solid',
'border-image-source': 'linear-gradient(' + deg + 'deg, #' + color2 + ',transparent)'
});
}
});
<body>
<form method="post" name="myemailform" action="callme.php">
<a href="index.html" class="article home">< Home</a>
<article class="article dear">DEAREST SACHIKO</article>
<textarea name="message" class="article body" placeholder="I recently came across a story you wrote in which trans people swap body parts after an online flesh trading solicitation is instantaneously granted. I know you walk a tight line between fiction and reality, so here goes nothing: I have a beard that might interest you. State your corporal offering and perhaps we can strike a deal.
" autofocus></textarea>
<input type="text" name="name" class="article from" placeholder="fondly, cybertrans anonymous">
<input type="email" name="email" class="article who" placeholder="your email address">
<input type="text" name="subject" class="article subject" placeholder="email subject">
<input type="submit" class="article send" name="submit" value="Send >"/>
</form>
</body>
CSS
form
{
display: grid;
grid-template-columns: 125px 1fr 1fr 125px;
grid-template-rows: 100px auto 50px 50px;
grid-template-areas:
"home dear dear send"
"home body body send"
"home from from send"
"home who subject send";
width: 100vw;
height: 100vh;
}
.article
{
padding: 5px 10px;
font-family: 'Hiragino Kaku Gothic ProN', sans-serif;
background-color: #a94d50;
border-bottom: 1px solid #ffe5be;
color: #ffe5be;
display: inline-block;
}
.dear
{
grid-area: dear;
font-family: 'Ballet';
font-size: 50px;
letter-spacing: 3px;
display: flex;
justify-content: center;
}
.dear::after
{
content: '幸子へ';
font-family: 'DotGothic16';
position: relative;
top: 0px;
margin: 0 auto;
writing-mode: vertical-rl;
text-orientation: upright;
font-size: 25px;
width: 30px;
height: 100px;
text-align: center;
}
.body
{
grid-area: body;
align-items: start;
padding: 10px 50px;
text-indent: 50px;
text-align: justify;
}
.from
{
grid-area: from;
display: flex;
justify-content: flex-end;
}
.who
{
grid-area: who;
display: flex;
justify-content: center;
}
.subject
{
grid-area: subject;
display: flex;
justify-content: center;
}
.send
{
grid-area: send;
border-left: 1px solid #ffe5be;
cursor: pointer;
}
.home
{
grid-area: home;
border-right: 1px solid #ffe5be;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.j
{
font-family: 'YuMincho' ,'Palatino', serif;
display: flex;
justify-content: flex-end;
}
textarea, input, textarea:focus, input:focus, a.home:hover
{
border: none;
outline: none;
resize: none;
background-color: #ffe5be;
border-bottom: 1px solid #ffe5be;
color: #a94d50;
}
textarea
{
box-sizing: border-box;
font-size: 25px;
}
a.home:hover
{
cursor: pointer;
}
*::placeholder
{
color: #ffe5be;
}
*:focus::placeholder
{
color: #a94d50;
}
a now-defunct online art collective
view in archive >$(document).ready(function(){
var z = 0;
$(document).mousemove(function(event){
var left = event.pageX,
top = event.pageY;
if (left%2 == 0 && z < 1000)
{
z++;
var fontSize = Math.floor(Math.random() * 40) + 15;
var c = Math.floor(Math.random() * 5) + 1;
jQuery('<a/>', {
href: 'documents/Submission%20Guidelines.docx',
class: 'popup style' + c,
css: {
'z-index': z,
'top': top,
'left': left,
'font-size' : fontSize,
'padding' : fontSize * 3 / 7,
'transform' : 'translate(-50%, -50%)',
'-webkit-transform' : 'translate(-50%, -50%)'
},
'text': 'Submit'
}).appendTo('body');
}
});
$(window).keypress(function (e) {
if (e.keyCode === 0 || e.keyCode === 32)
{
//clear (hit space)
e.preventDefault()
clear();
}
if (e.keyCode === 101 || e.keyCode === 69)
{
//epilepsy (hit escape)
e.preventDefault()
epilepsy();
}
if (e.keyCode >= 49 && e.keyCode <= 53)
{
//hide (hide 1-5)
e.preventDefault()
hide(e.keyCode);
}
});
function clear()
{
//allow buildup from beginning
$('.style1').remove();
$('.style2').remove();
$('.style3').remove();
$('.style4').remove();
$('.style5').remove();
z = 0;
}
function epilepsy()
{
clear();
z = 1001;
}
function hide(e)
{
if (e == 49)
{
$('.style1').toggle();
}
if (e == 50)
{
$('.style2').toggle();
}
if (e == 51)
{
$('.style3').toggle();
}
if (e == 52)
{
$('.style4').toggle();
}
if (e == 53)
{
$('.style5').toggle();
}
}
});
CSS
a
{
color: #FFF;
}
a.popup
{
position: absolute;
display: inline;
font-family: 'work sans', 'arial black', sans-serif;
font-weight: 700;
text-transform: uppercase;
text-decoration: none;
}
.style1
{
background-color: #ED5140;
box-shadow: 5px 5px #BF3021;
}
.style2
{
background-color: #86ED40;
box-shadow: 5px 5px #61BF21;
}
.style3
{
background-color: #A740ED;
box-shadow: 5px 5px #7F21BF;
}
.style4
{
background-color: #40DCED;
box-shadow: 5px 5px #21B0BF;
}
.style5
{
background-color: yellow;
color: #000;
box-shadow: 5px 5px #000;
}
function buffering(){
var windowWidth = $(window).outerWidth();
var totalWidth = 0;
var buffers = 0;
$('div').each(function(){
$(this).css('font-size', windowWidth/21.3);
var width = $(this).outerWidth();
var height = $(this).outerHeight();
totalWidth = totalWidth + width;
var currentDiv = $(this);
if (totalWidth > windowWidth)
{
//creating the buffer
var newDiv = $("<div class='buffer'></div>");
//placing it
if (buffers%2 == 0)
{
currentDiv.parent().prev().before(newDiv);
}
else
{
currentDiv.closest('a').before(newDiv);
}
buffers++;
//adjusting its width
var difference = totalWidth - windowWidth;
var newElementWidth = width - difference - 1;
newDiv.width(newElementWidth);
newDiv.height(height);
newDiv.css({padding: '0px'});
totalWidth = width;
}
});
var colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo'];
var index = 0;
$('div').hover(
function()
{
if (!$(this).hasClass('block'))
{
var color = colors[(index)%6];
$(this).css('background-color', color);
index++;
}
}, function()
{
if ($(this).hasClass('block'))
{
$(this).css('background-color', '#FFF');
}
}
);
}
$(window).resize(function(){
$('.buffer').remove();
buffering();
});
a home for BAMPFA's student club at UC Berkeley
view in archive >$(document).ready(function(){
var lastScrollTop = 0;
$(window).scroll(function(){
var scroll = $(window).scrollTop();
//scroll
if(scroll > 100 && scroll < 150 && scroll > lastScrollTop)
{
$("#bottombar").animate({'bottom': '0px'}, 800);
}
if (scroll == 0)
{
$("#bottombar").animate({'bottom': '-100px'}, 800);
}
lastScrollTop = scroll;
});
$("a[href='#top']").click(function(){
$('#bottombar .selected').removeClass('selected');
$('#bottombar .top').addClass('selected');
$('html, body').animate({scrollTop: 0}, 1000);
});
});
logos and other brand assets I have illustrated
view in archive >hand-lettered labels for a private library
view in archive >illustrated maps of the Bear Transit (UC Berkeley campus shuttles) and AC Transit systems as of 2019 (Prior to pandemic-related route changes)
view in archive >in-progress illustrated map of the AC Transit bus system
view in archive >made with pen, ink, pencil, and illustrator
please contact me to view >wears and wares by me, built on sewing machine
view online >a social media archive of my various calligraphy art
view online >a weaving together of Hebrew, Yiddish, and Arabic calligraphy
view in archive >various professional sites for freelance clients
Hilar Healtha visual journey through the chapbook The Mythology by Sachiko Ragosta, commissioned by the author
view online >a virtual home for a petition in support of UC Berkeley Bears Against ICE
view in archive >an archive of selected writings by poet and essayist Adonis Brooks
view in archive >a tumblr theme based on the old design of neopets.com
view online >tumblr theme
view online >