Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Stat Spark Lines & Energy Mix Pie Chart #116

Merged
merged 19 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/components/BarGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
v-html="graphTitle"
/>

<svg><!-- D3 inserts here --></svg>
<svg id="bar-graph"><!-- D3 inserts here --></svg>
</div>
</template>

Expand All @@ -21,10 +21,7 @@ export interface IGraphPoint {
/**
* A component that can graph an arbitrary array of numeric data
*/
@Component({
components: {
},
})
@Component({})
export default class BarGraph extends Vue {
@Prop({required: true}) graphTitle!: string;

Expand All @@ -48,7 +45,7 @@ export default class BarGraph extends Vue {
const outerHeight = this.height + this.graphMargins.top + this.graphMargins.bottom;

this.svg = d3
.select("svg")
.select("svg#bar-graph")
.attr("width", outerWidth)
.attr("height", outerHeight)
.attr("viewBox", `0 0 ${outerWidth} ${outerHeight}`)
Expand Down
2 changes: 1 addition & 1 deletion src/components/BuildingImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default class BuildingImage extends Vue {

img {
border-radius: $brd-rad-medium;
max-height: 30rem;
max-height: 25rem;
}

.attribution {
Expand Down
56 changes: 26 additions & 30 deletions src/components/DataDisclaimer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,28 @@
unless explicitly stated otherwise.
</summary>

<p class="constrained">
<strong>Note:</strong> This data only includes buildings whose emissions are reported
under the
<a
href="https://www.chicago.gov/city/en/progs/env/building-energy-benchmarking---transparency.html"
target="_blank"
rel="noopener"
>
Chicago Energy Benchmarking Ordinance<NewTabIcon />
</a>. According to the City &ldquo;As of 2016,
this list includes all commercial, institutional, and residential buildings larger than
50,000 square feet.&rdquo; This dataset is also then filtered to only buildings with
reported emissions > 1,000 metric tons CO<sub>2</sub> equivalent.
</p>

<p class="constrained">
The latest year of data is from {{ LatestDataYear }}, but we update the site regularly when
new data is available, and some buildings may have failed to report that year, and only have
older data available.
</p>
<div class="details-content">
<p class="constrained">
<strong>Note:</strong> This data only includes buildings whose emissions are reported
under the
<a
href="https://www.chicago.gov/city/en/progs/env/building-energy-benchmarking---transparency.html"
target="_blank"
rel="noopener"
>
Chicago Energy Benchmarking Ordinance<NewTabIcon />
</a>. According to the City &ldquo;As of 2016,
this list includes all commercial, institutional, and residential buildings larger than
50,000 square feet.&rdquo; This dataset is also then filtered to only buildings with
reported emissions > 1,000 metric tons CO<sub>2</sub> equivalent.
</p>

<p class="constrained">
The latest year of data is from {{ LatestDataYear }}, but we update the site regularly when
new data is available, and some buildings may have failed to report that year, and only have
older data available.
</p>
</div>
</details>
</template>

Expand All @@ -49,19 +51,13 @@ export default class DataDisclaimer extends Vue {

<style lang="scss">
.data-disclaimer {
background: $grey;
border-radius: $brd-rad-medium;
margin-bottom: 1rem;
font-size: 0.825rem;

summary, p { padding: 0.5rem 1rem; }
summary, .details-content { padding: 0.5rem 1rem; }

summary {
font-weight: bold;
font-size: 0.825rem;
}
summary { font-weight: bold; }

p {
margin-top: 0.5rem;
}
p { margin-top: 0.5em; }
}
</style>
171 changes: 171 additions & 0 deletions src/components/PieChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<div class="pie-chart-cont">
<svg id="pie-chart"><!-- D3 inserts here --></svg>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import * as d3 from 'd3';

export interface IPieSlice {
value: number;
label: string;
color: string;
}

/**
* A component that can graph an arbitrary array of numeric data as a pie chart
*
* E.g. Votes for favorite pies:
* [ { label: 'Cherry', value: 30 }, { label: 'Chocolate', value: 12 }]
*/
@Component({})
export default class PieChart extends Vue {
@Prop({required: true}) graphData!: Array<IPieSlice>;

@Watch('graphData')
onDataChanged(): void {
this.renderGraph();
}

readonly width = 400;
readonly height = 320;

readonly graphMargins = { top: 0, right: 0, bottom: 0, left: 0 };

svg!: d3.Selection<SVGGElement, unknown, HTMLElement, any>;

Check warning

Code scanning / ESLint

Disallow the `any` type Warning

Unexpected any. Specify a different type.

mounted(): void {
const outerWidth = this.width + this.graphMargins.left + this.graphMargins.right;
const outerHeight = this.height + this.graphMargins.top + this.graphMargins.bottom;

this.svg = d3
.select("svg#pie-chart")
.attr("width", outerWidth)
.attr("height", outerHeight)
.attr("viewBox", `0 0 ${outerWidth} ${outerHeight}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.append("g")
.attr("transform", `translate(${this.width / 2},${this.height / 2})`);

this.renderGraph();
}

renderGraph(): void {
// Empty the SVG
this.svg.html(null);

const radius = 100;
const labelRadius = 150;

// Compute the position of each group on the pie:
var pie = d3.pie()
.value((d: any) => d.value);

Check warning

Code scanning / ESLint

Disallow the `any` type Warning

Unexpected any. Specify a different type.
var dataReady = pie(this.graphData as any);

Check warning

Code scanning / ESLint

Disallow the `any` type Warning

Unexpected any. Specify a different type.

// shape helper to build arcs:
var arcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(radius);

var labelArcGenerator = d3.arc()
.innerRadius(radius)
.outerRadius(labelRadius);

// Build the pie chart: Basically, each part of the pie is a path that we build using the
// arc function.
this.svg.selectAll('mySlices')
.data(dataReady)
.enter()
.append('path')
.attr('d', arcGenerator as any)

Check warning

Code scanning / ESLint

Disallow the `any` type Warning

Unexpected any. Specify a different type.
.attr('fill', (d) => (d.data as unknown as IPieSlice).color);


// Calculate total value for % calculation
let totalValue = 0;
this.graphData.forEach((d) => totalValue += d.value);

/** Add pie chart labels */
this.svg.selectAll('mySlices')
.data(dataReady)
.enter()
.append('text')
.html((d) => {
// Convert degrees to rads
const thresholdRadians = 5 / 360 * 2 * Math.PI;

// If we have a lot of small slices, we skip those labels so they don't collide
if (this.graphData.length > 2
&& (d.endAngle - d.startAngle) < thresholdRadians) {
return '';
}

let data = d.data as any as IPieSlice;

Check warning

Code scanning / ESLint

Disallow the `any` type Warning

Unexpected any. Specify a different type.

const label =
`<tspan class="percent">${this.calculatePercentage(data.value, totalValue)}%</tspan>` +
`<tspan class="label" x="0" dy="1.5em">${data.label}</tspan>`;

return label;
})
.attr('class', () => this.graphData.length === 1 ? '-only-slice' : '')
.attr("transform", (d) => {
// If we have only 1 slice (e.g. 100% electric, like Marina Towers), place dead center,
// otherwise use secondary arc centroid
if (this.graphData.length === 1) {
return '';
}

return `translate(${labelArcGenerator.centroid(d as unknown as d3.DefaultArcObject)})`;
})
.style("text-anchor", (d) => {
// Center single slice label
if (this.graphData.length === 1) { return 'middle'; }

// are we past the center?
return (d.endAngle + d.startAngle) / 2 > Math.PI ?
"end" : "start";
});
}

calculatePercentage(value: number, total: number): string {
const percentage = value / total * 100;

if (percentage < 1) {
return '< 1';
}
// If > 99%, we don't want to round to 100% so we can show there's other slices
else if (percentage > 99 && percentage < 100) {
return Math.floor(percentage).toString();
}
else {
return Math.round(percentage).toString();
}
}
}
</script>

<style lang="scss">
.pie-chart-cont {
svg {
width: 100%;
height: auto;
max-width: 50rem;

path {
stroke: $white;
stroke-width: 0.25rem;
}

text.-only-slice { font-size: 1.5rem; }
}

tspan.percent {
font-weight: bold;
font-size: 1.3em;
}
tspan.label { font-size: 0.65em; }
}
</style>
Loading
Loading