Published
Edited
Apr 12, 2021
Insert cell
Insert cell
videoRecording = FileAttachment("Screen Recording 2021-02-26 at 20.46.28.mov")
Insert cell
Insert cell
Insert cell
Insert cell
width = 660
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Performing data transformation by changing the key names and parsing dates and numbers (parseDate funtion at the end of the document)
data = {
const URL = "https://data.civio.es/quiencobralaobra/contratacion-menores/obras-res-obras.csv";
const data = await d3.csv(URL, ({feed_id, budgeted_tax_exclusive_amount, awarded_tax_exclusive_amount, tender_award_date}) => ({id: feed_id, budgetedTax: +budgeted_tax_exclusive_amount, date: parseDate(tender_award_date)}))
data.x = "Periodo de tiempo registrado";
data.y = "Importes de los contratos menores $$";
return data;
}
Insert cell
Insert cell
dataFiltered = {
const cutoffDate = new Date(2019,7,1); // Cuttoff date (01/08/2019)
const dataFiltered = data
.filter(d => (d.budgetedTax <= 60000) && (d.budgetedTax >= taxDomainSelector))
.filter(d => d.date < cutoffDate)
.slice().sort((a,b) => d3.ascending(a.date, b.date));
return dataFiltered;
}
Insert cell
Insert cell
allDateDataDomain = d3.extent(dataFiltered, d => d.date)
Insert cell
allDateDataDomainNice = {
// return [new Date(2018,0,1), new Date(2019,7,1)]
return [new Date(2018,0,1), new Date(2019,7,1)]

}
Insert cell
allTaxesDomain = d3.extent(dataFiltered, d => d[taxSelector])
Insert cell
Insert cell
xTime = d3.scaleTime() // Continuous time scale
// .domain([dateDataDomain[0], d3.timeMonth.ceil(dateDataDomain[1])+1])
// .domain(dateDataDomain).nice() // To get nice start and end values "rounding" the domain
.domain(allDateDataDomainNice)
.range([margin.left, width - margin.right])
Insert cell
yTaxes = d3.scaleLinear()
.domain(allTaxesDomain)
.range([height - margin.bottom, margin.top])
Insert cell
Insert cell
// Custom function to pack the data the way we want
histogramTime = d3.histogram()
.value(d => d.date) // The vector of value (TIME!)
.domain(xTime.domain())
// .thresholds(xTime.ticks(detailBinsTime)) // Number of bins (user choice via slider)
// .thresholds(xTime.ticks(19))
.thresholds(xTime.ticks(d3[detailBinsTimeIntervals]))
//.theresholds(xTime.ticks(detailBinsTimeIntervals))
Insert cell
// Apply the histogram function to our data to get the bins
binsTime = histogramTime(dataFiltered)
Insert cell
// Custom function to pack the data the way we want
histogramTaxes = d3.histogram()
.value(d => d[taxSelector]) // The vector of value (TAXES: user choice via selector)
.domain(yTaxes.domain())
.thresholds(yTaxes.ticks(detailBinsTaxes)); // Number of bins (user choice via slider)
Insert cell
// Apply the histogram function to each item of our packed data to get the bins on a second level
binsTaxes = binsTime
.map(d => histogramTaxes(d))
Insert cell
binsTaxes
.forEach((d,i) => {
d.x0 = binsTime[i].x0;
d.x1 = binsTime[i].x1;
})
Insert cell
//binsTaxes
//.forEach((d,i) => d.x1 = binsTime[i].x1)
Insert cell
binsTaxes
Insert cell
binsTaxes[50].x1
Insert cell
Insert cell
xAxisTimeMonths = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xTime)
// .tickFormat(d3.timeFormat('%b %Y'))
.tickFormat(d3.timeFormat('%b'))
// .tickFormat(d => d <= d3.timeYear(d) ? d.getFullYear() : d.getMonth())
//.ticks(d3.timeMonth.every(1))
.ticks(d3.timeMonth)
)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", width - margin.right)
.attr("y", 35)
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.text(data.x))
Insert cell
// Mark the years!
xAxisTimeYears = g => g
.attr("transform", `translate(0,${height - margin.bottom + 25})`)
.call(d3.axisBottom(xTime)
.ticks(d3.timeYear)
.tickSize(0))
.call(g => g.select(".domain")
.remove())
.call(g => g.selectAll("g text")
.attr("font-weight", "bold")
.attr("font-size", "12px"))
Insert cell
yAxisTaxes = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yTaxes)
//.tickValues([3000, 6000, 9000, 12000, 15000, 18000]) // Custom tick spacing
)
.call(g => g.select(".domain").remove())
// .call(g => g.select(".tick:last-of-type text").clone()
.call(g => g.append("text")
// .attr("x", 5)
// .attr("y", -60)
.attr("x", -margin.left)
.attr("y", -margin.top)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("font-weight", "bold")
.text(data.y))
Insert cell
Insert cell
colorScale = d3.scaleSequential()
.domain([0, colorLevel]) // Custom cutoff
// .domain([0, d3.max(binsTimeMultiple.flat(), d => d.length)]).nice() // Aquí los histogramas según se empaquete
.interpolator(d3["interpolate" + colorScheme])
Insert cell
d3.max(binsTaxes.flat(), d => d.length)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
infoItems = itemId => dataFiltered.find(element => element.id === itemId);
Insert cell
infoItems("3748152")
Insert cell
infoItems("3044012")
Insert cell
// Based on https://observablehq.com/@hydrosquall/d3-annotation-with-d3-line-chart
annotations = {
const item01 = '3748152';
const item02 = '3044012';

// Helper function to lookup where annotations match values
const infoItems = itemId => dataFiltered.find(element => element.id === itemId);
infoItems(item01) ;
return [{
id: infoItems(item01).id,
// If you don't provide a custom "type" attribute in your options dictionary, ,
// the default type in the getAnnotations function will be used.
note: {
title: `Servicio de Salud de las Illes Balears (${infoItems(item01).budgetedTax}€)`,
label: "Empresa: CROITORU VICTOR",
wrap: 250
},
x: xTime(infoItems(item01).date),
y: yTaxes(infoItems(item01).budgetedTax),
dx: 50,
dy: -50,
connector: {
end: "dot", // 'arrow'/'dot'
endScale: 0.5
},
},
{
id: infoItems(item02).id,
note: {
title: `Servicio de Salud de las Illes Balears (${infoItems(item02).budgetedTax}€)`,
label: "Empresa: CROITORU VICTOR",
wrap: 250
},
x: xTime(infoItems(item02).date),
y: yTaxes(infoItems(item02).budgetedTax),
dx: 50,
dy: -50,
connector: {
end: "dot", // 'arrow'/'dot'
endScale: 0.5
},
}
]
.map(d => { d.color = "black"; return d; })
}
// I used 4 of the 8 annotation types, be sure to see the other 4 options when building your own!
// http://d3-annotation.susielu.com/#types
Insert cell
//makeAnnotations = d3.annotation().type(d3.annotationLabel).annotations(annotations)
makeAnnotations = d3.annotation()
.editMode(false)
/*
.accessors({
x: d => xTime(parseDate(d.date)),
y: d => yTaxes(d.budgetedTax)
})
.accessorsInverse({
date: d => formatDate(xTime.invert(d.x)),
budgetedTax: d => yTaxes.invert(d.y)
})
*/

.type(d3.annotationLabel)
.annotations(annotations)
Insert cell
Insert cell
styles = html`
<style>

/*
.y-axis-label,
.x-axis-label {
font-family: Lato, Arial, sans-serif;
font-size: 12px;
text-transform: uppercase;
}

.y-axis line {
stroke-dasharray: 2 2;
stroke: #bababa;
}

.y-axis .domain {
fill-opacity: 0;
stroke-opacity: 0;
}
*/

// Chart styles
.active {
stroke: black!important;
stroke-width: 3;
// visibility: hidden;
}

.tooltip {
display: block;
position: absolute;
z-index: 300;
visibility: hidden;
max-width: 320px;
// min-width: 150px;
margin: 0;
padding: 0.4rem;

// background: rgba(69,77,93,.9);
// color: #fff;
background: rgba(255, 255, 255, 0.9);
color: black;
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
border-radius: 0.2rem;


font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 11px;
// line-height: 1.4;
text-align: center;

// text-overflow: ellipsis;
// white-space: pre-wrap;
}

// Tooltip styles
.tooltip-title {
color: black;
font-size: 14px
}
.tooltip-text {
color: blue;
}

.subset1 {
opacity: 0.1;
}
.annotation-note-title, .axis-label {
font-weight: bold;
}

//Annotations contracts
.contract {
background-color: lightgrey;
}

.active {
//border-width: 1.5px;
//border-color: black;
background-color: #76FDA9;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more