Public
Edited
Sep 3, 2022
Insert cell
md`# Graph Design - Exercise_updated (Film Permits)

In this exercise, we will be working with the [NYC Open Data Film Permits dataset](https://data.cityofnewyork.us/City-Government/Film-Permits/tg4x-b46p). For each problem below, create a visualization that provides an answer to the questions. Also write a short justification of your visualization design choices.

To get started, create a fork of this notebook. When you are finished, **do not** publically publish your notebook. If you are using the "Classic" notebook interface, then go to the menu at the top with the three dots and click "Enable link sharing." If you are using the newer notebook interface, then click "Publish" and set the visibility to unlisted. You can then submit the link on Brightspace.`
Insert cell
Insert cell
Insert cell
Insert cell
data = (await FileAttachment('film_permits_reduced.csv').csv())
.map(permit => {
// create a function to parse strings into Dates
const parser = d3.timeParse('%Y-%m-%d %H:%M:%S');
permit['permitDuration'] = d3.timeHour.count(new Date(permit['StartDateTime']), new Date(permit['EndDateTime']));
permit['StartDateTime'] = parser(permit['StartDateTime']);
permit['EndDateTime'] = parser(permit['EndDateTime']);
//const startDate = new Date(permit['StartDateTime'])
permit['StartMonth'] = d3.timeFormat('%b')(permit['StartDateTime']);
permit['StartYear'] = d3.timeFormat('%Y')(permit['StartDateTime']);
// a permit can be for multiple zip codes
permit['ZipCodes'] = permit['ZipCode(s)'].split(', ');
// remove columns that we are not using it
permit['SubCategoryName'];
delete permit['ZipCode(s)'];
return permit;
})
Insert cell
Insert cell
d3.timeFormat('%Y')(data[0].StartDateTime)
Insert cell
d3.timeFormat('%b')(data[0].StartDateTime)
Insert cell
zipData = []
Insert cell
timeSortedData = data.sort((a,b) => d3.ascending(a.StartDateTime,b.EndDateTime))
Insert cell
Inputs.table(timeSortedData)
Insert cell
Insert cell
parser = d3.timeParse('%Y-%m-%d %H:%M:%S')
Insert cell
d3.timeHour.count(new Date("2018-10-12T19:30Z") , new Date("2018-10-13T19:30Z"))
Insert cell
permDur = d3.rollups(data, g => d3.sum(g, v => v.permitDuration), d => d.Category).sort((a,b) => d3.descending(a[1],b[1]))
Insert cell
prob1 = {
const chart = d3.create('svg').attr('width',width).attr('height',height)
const visual = chart.append('g')
.attr('transform',`translate(${margin.left},${margin.top})`)

const xScale = d3.scaleBand()
.domain(permDur.map(d => d[0]))
.range([0,visWidth])

const xAxis = chart.append('g').attr('transform',`translate(${margin.left},${height - margin.bottom})`)
.call(d3.axisBottom(xScale))

const yScale = d3.scaleLinear()
.domain(d3.extent(permDur.map(d => +d[1])))
.range([visHeight,0])

const yAxis = chart.append('g').attr('transform',`translate(${margin.left},20)`)
.call(d3.axisLeft(yScale))

const categColor = d3.scaleOrdinal()
.domain(permDur.map(d => d[0]))
.range(d3.schemePastel1)
const bars = visual.selectAll('rect')
.data(permDur)
.join('rect').attr('x', d => xScale(d[0]))
.attr('width',xScale.bandwidth)
.attr('y',d => yScale(d[1]))
.attr('height', d => visHeight - yScale(d[1]))
.attr('fill', d => categColor(d[0]))
return chart.node()
}
Insert cell
Television permits are still ranking at the top based on the Permit Duration. With only 10 categories, using bars to show the duration of the permit is easiest.
Insert cell
visWidth = width - margin.left - margin.right
Insert cell
visHeight = height - margin.bottom - margin.top
Insert cell
margin = ({top:20,right:20,left:50,bottom:40})
Insert cell
width = 900
Insert cell
height = 800
Insert cell
Insert cell
In each zipCode what are the categories permit are given, and how many permits are there in each Zip code

Group based on the Zipcode and the Categories and find the numbers. Start with only the Categories and the ZipCodes
Insert cell
zipC = {
const zipCategory = [];
timeSortedData.forEach(d => {
d.ZipCodes.forEach(z =>
zipCategory.push({
zip:z,
permitDuration: d.permitDuration,
category: d.Category,
borough: d.Borough,
StartMonth: d.StartMonth,
StartYear: d.StartYear
}))
})
return zipCategory
}
Insert cell
Inputs.table(zipC)
Insert cell
//Which zip has most Permit
zipPermit = d3.rollups(zipC, g => g.length, d => d.zip).sort((a,b) => d3.descending(a[1],b[1]))
Insert cell
viewof zips = Inputs.range([0,zipPermit.length],{label:"Number Of Codes",step:1,value:10})
Insert cell
prob2 = {
const chart = d3.create('svg').attr('width',width).attr('height',height)
const visual = chart.append('g')
.attr('transform',`translate(${margin.left},${margin.top})`)

const yScale = d3.scaleBand()
.domain(zipPermit.slice(0,zips).map(d => d[0]))
.range([0,visHeight])

const yAxis = chart.append('g').attr('transform',`translate(${margin.left},20)`)
.call(d3.axisLeft(yScale))

const xScale = d3.scaleLinear()
.domain(d3.extent(zipPermit.slice(0,zips).map(d => +d[1])))
.range([0,visWidth])

const xAxis = chart.append('g').attr('transform',`translate(${margin.left},${height - margin.bottom})`)
.call(d3.axisBottom(xScale))

const zipColor = d3.scaleOrdinal()
.domain(zipPermit.slice(0,zips).map(d => d[0]))
.range(d3.schemePastel1)
const bars = visual.selectAll('rect')
.data(zipPermit.slice(0,zips))
bars
.join(
enter => enter.append('rect')
.attr('x', 0)
.attr('width',d => xScale(d[1]))
.attr('y',d => yScale(d[0]))
.attr('height', yScale.bandwidth)
.attr('fill', d => zipColor(d[0])),
update => update
.call(
y => y.transition()
.duration(1500)
.attr('width',d => xScale(d[1]))
),
exit => exit.attr('fill', 'red')
.call(
e => e.transition()
.duration(1000)
.attr('opacity', 0)
.remove()
)
)
return chart.node()
}
Insert cell
//How many zipCodes the permit is provided?
d3.rollup(data, g =>d3.count(g, y => y.ZipCodes), d => d.Category)
Insert cell
d3.count(data[5].ZipCodes)
Insert cell
d3.rollups(data, g => g.length, z => z.ZipCodes, v => v.Category)
Insert cell
import {aq, op} from "@uwdata/arquero"
Insert cell
filmPermit = aq
.fromCSV(await FileAttachment("film_permits_reduced.csv").text())
Insert cell
splitFilm = filmPermit
.spread({ key: d => op.split(d["ZipCode(s)"],',') })
.view(10)
Insert cell
Insert cell
borough = d3.rollups
(data, g => g.length, d => d.Borough, d => d.Category)
Insert cell
[...borough.entries()].map(([b,data])=> ({borough:b,cat:data}))
Insert cell
Inputs.table(borough)
Insert cell
manhattan = borough[0][1].map(([category,value])=>({category, value})).sort((a,b) => d3.descending(a.value, b.value))
Insert cell
boroughCat = borough.map(m => m[1].map(([category,value])=>({category, value})).sort((a,b) => d3.descending(a.value, b.value)))
Insert cell
{
const townCat = [];
borough.forEach(d => {
townCat.push(d[1].map(([category,value]) => ({category,value})))
})
return townCat
}
Insert cell
townCat = {
const townCat = [];
borough.forEach(d => {
d[1].forEach(([category,value]) =>
townCat.push({
category: category,
value: value,
borough : d[0]}))
})
return townCat
}
Insert cell
boroughPermitMap = d3.rollup
(data, g => g.length, d => d.Borough, d => d.Category)
Insert cell
[...townCat.entries()]
Insert cell
Inputs.table(townCat.sort((a,b) => d3.descending(a.value,b.value)))
Insert cell
boroughList = [...new Set(townCat.map(d => d.borough))]
Insert cell
categoryList = [...new Set(townCat.map(d => d.category))]
Insert cell
maxValueBorough = d3.max(townCat.map(d => d.value))
Insert cell
import {swatches} from '@d3/color-legend'
Insert cell
color = d3.scaleOrdinal()
.domain(categoryList)
.range(d3.schemeTableau10);
Insert cell
swatches({color})
Insert cell
// this scale will be used to position the groups for each borough in a vertically
groupChart = {
const base = d3.create('svg').attr('width',width).attr('height',height)

const chart = base.append('g').attr('transform',`translate(50,${margin.top})`)
const group = d3.scaleBand()
.domain(boroughList)
.range([visHeight, 0])
.padding(0.2);
const x = d3.scaleLinear()
.domain([0, maxValueBorough]).nice()
.range([0, visWidth]);
// this scale will be used to position the bars within a group
const y = d3.scaleBand()
.domain(categoryList)
.range([0, group.bandwidth()])
.padding(0.2);
// set up the axes
const yAxis = d3.axisLeft(group);
const xAxis = d3.axisBottom(x);

chart.append('g')
.attr('transform', `translate(0,${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove());
chart.append("g")
.attr('transform', `translate(0,0)`)
.call(yAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('fill', 'black')
.attr('text-anchor', 'start')
.attr('dominant-baseline', 'hanging')
.attr('font-weight', 'bold')
.attr('y', -margin.top + 5)
.attr('x', -margin.left)
.text('Zip Code Permits')
// create and position one group for each month
const groups = chart.append('g')
.selectAll('g')
.data(boroughPermitMap)
.join('g')
.attr('transform', ([borough, categoryPermit]) => `translate(0,${group(borough)})`);
// add the bars to each group
groups.selectAll('rect')
.data(([borough, categoryPermit]) => categoryPermit)
.join('rect')
.attr('fill', ([category,value]) => color(category))
.attr('y', ([category,value]) => y(category))
.attr('height', y.bandwidth())
.attr('x', 0)
.attr('width', ([category,value]) => x(value))

return base.node()
}
Insert cell
Insert cell
- Q_part1 : How many permits start every month and how does the quantity change over the month?
- Q_part2 : Does the change of quantity differ every year?
Insert cell
yearlyTrend= d3.rollup(data, g => g.length, d => d.StartYear, d => d.StartMonth)
Insert cell
yearlyTrendArray = d3.rollups(data, g => g.length, d => d.StartYear, d => d.StartMonth)
Insert cell
years = [...yearlyTrend.keys()]
.sort((a,b) => d3.ascending(+a, +b))
Insert cell
months = [...yearlyTrend.get('2015').keys()].sort()
Insert cell
scaleMonth = d3.scaleOrdinal()
.domain(months)
.range(d3.range(months.length).map(d => d * 50))
Insert cell
colorYear = d3.scaleOrdinal()
.domain(years)
.range(d3.schemeDark2)
Insert cell
[...yearlyTrend.entries()]
Insert cell
monthlyValues = {
const counts = [];
yearlyTrend.forEach((month,year) => {
//counts.push({d,v})
month.forEach((value,month)=>{ //the value is first and then key
counts.push(value)
})
})
return counts
}
Insert cell
countScale = d3.scaleLinear()
.domain([0,d3.max(monthlyValues)])
.range([visHeight,0])
Insert cell
line = d3.line()
.x(([mon, val]) => scaleMonth(mon))
.y(([mon, val]) => countScale(val))
Insert cell
yearlyTrendArray.map(d => d[0])
Insert cell
yearlyTrendArray[0][1].map(([mon, val]) => val)
Insert cell
yearlyTrendArray[0][1].map(([mon, val]) => mon)
Insert cell
line(yearlyTrendArray[0][1])
Insert cell
yearlyTrendArray.map(([year,month])=>line(month))
Insert cell
yearlyTrendArray.filter(d => d[0] == yr)[0][1].sort((a,b) => (a[1] - b[1]))
Insert cell
viewof yr = Inputs.select(years,{label:"year",value:2015})
Insert cell
monthTrend = {
const chart = d3.create('svg').attr('width',width).attr('height',height)

const visual = chart.append('g')
.attr('transform',`translate(${margin.left},${margin.top})`)

const xAxis = chart.append('g')
.attr('transform',`translate(${margin.left},${visHeight})`)
.call(d3.axisBottom(scaleMonth))

const yAxis = chart.append('g')
.attr('transform',`translate(${margin.left},0)`)
.call(d3.axisLeft(countScale))

const trend = visual.selectAll('g')
.data(yearlyTrendArray.filter(d => d[0] == yr))
.join('g')
.attr('fill',d => colorYear(d[0]))

trend.append('path')
.attr('d', d => line(d[1]))
.attr('stroke', d => colorYear(d[0]))
.attr('stroke-width', 2)
.attr('fill','none')

const legend = chart.append('g')
.attr('transform',`translate(75,75)`)
legend
.selectAll('rect')
.data(years)
.join('rect')
.attr('x',0)
.attr('height',10)
.attr('width',10)
.attr('y', (d,i) => i * 20)
.attr('fill', d => colorYear(d))
legend
.selectAll('text')
.data(years)
.join('text')
.attr('x',20)
.attr('y', (d,i) => i * 20)
.attr('dy',10)
.text(d => `:${d}`)
.attr('fill', 'black')
return chart.node()
}
Insert cell
Insert cell
dispatcher = d3.dispatch('year')
Insert cell
yearScaleLin = d3.scaleLinear()
.domain(years)
.range(d3.range(years.length).map(d => d * 50))
Insert cell
yearDrag = dispatcher.on('year',function() {
const temp = Math.floor(yearScaleLin.invert(this.attr('cx')))
return temp
})
Insert cell
dragYear = {

const chart = d3.create('svg').attr('width',300).attr('height',200)
const infoYear = chart.append('text')
.attr('transform',`translate(170,25)`)
.attr('id','info')

const yearScale = d3.scaleLinear()
.domain(years.map(d => +d))
.range(d3.range(years.length).map(d => d * 50))
const yearAxis = chart.append('g').attr('transform',`translate(50,100)`)
.call(d3.axisBottom(yearScale)
.ticks(5))

const pointer = chart.append('circle')
.attr('cx',50)
.attr('cy',100)
.attr('r',10)
.attr('fill','green')
.attr('opacity',0.8)
.attr('stroke','black')

const dragUpdt = d3.drag()
.on('start',dragStart)
.on('drag',dragging)
.on('end',dragEnd);
console.log(dragging)

function dragStart(){
d3.select('this')
.raise()
.attr('stroke','black')
}
function dragging(event){
d3.select(this)
.attr('cx',() =>{
if (event.x > years.length*50){
return years.length * 50
} else if (event.x <= 50){
return 50
} else {
return event.x
}
})
var pos = d3.pointer(event);
var xPos = pos[0];
var value = yearScaleLin.invert(xPos) - 2;
d3.select('#info')
.text('Your Year ' + Math.ceil(value));
return value
}
function dragEnd(){
d3.select(this).attr('stroke',null)
}
pointer.call(dragUpdt)
return chart.node()
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more